| @@ -0,0 +1,437 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (C) 2010 The Android Open Source Project | |
| 3 | + | * | |
| 4 | + | * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | + | * you may not use this file except in compliance with the License. | |
| 6 | + | * You may obtain a copy of the License at | |
| 7 | + | * | |
| 8 | + | * http://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | + | * | |
| 10 | + | * Unless required by applicable law or agreed to in writing, software | |
| 11 | + | * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | + | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | + | * See the License for the specific language governing permissions and | |
| 14 | + | * limitations under the License. | |
| 15 | + | * | |
| 16 | + | */ | |
| 17 | + | ||
| 18 | + | #include <jni.h> | |
| 19 | + | ||
| 20 | + | #include <errno.h> | |
| 21 | + | #include <stdlib.h> | |
| 22 | + | #include <string.h> | |
| 23 | + | #include <unistd.h> | |
| 24 | + | #include <sys/resource.h> | |
| 25 | + | ||
| 26 | + | #include "android_native_app_glue.h" | |
| 27 | + | #include <android/log.h> | |
| 28 | + | ||
| 29 | + | #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__)) | |
| 30 | + | #define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__)) | |
| 31 | + | ||
| 32 | + | /* For debug builds, always enable the debug traces in this library */ | |
| 33 | + | #ifndef NDEBUG | |
| 34 | + | # define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__)) | |
| 35 | + | #else | |
| 36 | + | # define LOGV(...) ((void)0) | |
| 37 | + | #endif | |
| 38 | + | ||
| 39 | + | static void free_saved_state(struct android_app* android_app) { | |
| 40 | + | pthread_mutex_lock(&android_app->mutex); | |
| 41 | + | if (android_app->savedState != NULL) { | |
| 42 | + | free(android_app->savedState); | |
| 43 | + | android_app->savedState = NULL; | |
| 44 | + | android_app->savedStateSize = 0; | |
| 45 | + | } | |
| 46 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | int8_t android_app_read_cmd(struct android_app* android_app) { | |
| 50 | + | int8_t cmd; | |
| 51 | + | if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) { | |
| 52 | + | switch (cmd) { | |
| 53 | + | case APP_CMD_SAVE_STATE: | |
| 54 | + | free_saved_state(android_app); | |
| 55 | + | break; | |
| 56 | + | } | |
| 57 | + | return cmd; | |
| 58 | + | } else { | |
| 59 | + | LOGE("No data on command pipe!"); | |
| 60 | + | } | |
| 61 | + | return -1; | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | static void print_cur_config(struct android_app* android_app) { | |
| 65 | + | char lang[2], country[2]; | |
| 66 | + | AConfiguration_getLanguage(android_app->config, lang); | |
| 67 | + | AConfiguration_getCountry(android_app->config, country); | |
| 68 | + | ||
| 69 | + | LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d " | |
| 70 | + | "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d " | |
| 71 | + | "modetype=%d modenight=%d", | |
| 72 | + | AConfiguration_getMcc(android_app->config), | |
| 73 | + | AConfiguration_getMnc(android_app->config), | |
| 74 | + | lang[0], lang[1], country[0], country[1], | |
| 75 | + | AConfiguration_getOrientation(android_app->config), | |
| 76 | + | AConfiguration_getTouchscreen(android_app->config), | |
| 77 | + | AConfiguration_getDensity(android_app->config), | |
| 78 | + | AConfiguration_getKeyboard(android_app->config), | |
| 79 | + | AConfiguration_getNavigation(android_app->config), | |
| 80 | + | AConfiguration_getKeysHidden(android_app->config), | |
| 81 | + | AConfiguration_getNavHidden(android_app->config), | |
| 82 | + | AConfiguration_getSdkVersion(android_app->config), | |
| 83 | + | AConfiguration_getScreenSize(android_app->config), | |
| 84 | + | AConfiguration_getScreenLong(android_app->config), | |
| 85 | + | AConfiguration_getUiModeType(android_app->config), | |
| 86 | + | AConfiguration_getUiModeNight(android_app->config)); | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) { | |
| 90 | + | switch (cmd) { | |
| 91 | + | case APP_CMD_INPUT_CHANGED: | |
| 92 | + | LOGV("APP_CMD_INPUT_CHANGED\n"); | |
| 93 | + | pthread_mutex_lock(&android_app->mutex); | |
| 94 | + | if (android_app->inputQueue != NULL) { | |
| 95 | + | AInputQueue_detachLooper(android_app->inputQueue); | |
| 96 | + | } | |
| 97 | + | android_app->inputQueue = android_app->pendingInputQueue; | |
| 98 | + | if (android_app->inputQueue != NULL) { | |
| 99 | + | LOGV("Attaching input queue to looper"); | |
| 100 | + | AInputQueue_attachLooper(android_app->inputQueue, | |
| 101 | + | android_app->looper, LOOPER_ID_INPUT, NULL, | |
| 102 | + | &android_app->inputPollSource); | |
| 103 | + | } | |
| 104 | + | pthread_cond_broadcast(&android_app->cond); | |
| 105 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 106 | + | break; | |
| 107 | + | ||
| 108 | + | case APP_CMD_INIT_WINDOW: | |
| 109 | + | LOGV("APP_CMD_INIT_WINDOW\n"); | |
| 110 | + | pthread_mutex_lock(&android_app->mutex); | |
| 111 | + | android_app->window = android_app->pendingWindow; | |
| 112 | + | pthread_cond_broadcast(&android_app->cond); | |
| 113 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 114 | + | break; | |
| 115 | + | ||
| 116 | + | case APP_CMD_TERM_WINDOW: | |
| 117 | + | LOGV("APP_CMD_TERM_WINDOW\n"); | |
| 118 | + | pthread_cond_broadcast(&android_app->cond); | |
| 119 | + | break; | |
| 120 | + | ||
| 121 | + | case APP_CMD_RESUME: | |
| 122 | + | case APP_CMD_START: | |
| 123 | + | case APP_CMD_PAUSE: | |
| 124 | + | case APP_CMD_STOP: | |
| 125 | + | LOGV("activityState=%d\n", cmd); | |
| 126 | + | pthread_mutex_lock(&android_app->mutex); | |
| 127 | + | android_app->activityState = cmd; | |
| 128 | + | pthread_cond_broadcast(&android_app->cond); | |
| 129 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 130 | + | break; | |
| 131 | + | ||
| 132 | + | case APP_CMD_CONFIG_CHANGED: | |
| 133 | + | LOGV("APP_CMD_CONFIG_CHANGED\n"); | |
| 134 | + | AConfiguration_fromAssetManager(android_app->config, | |
| 135 | + | android_app->activity->assetManager); | |
| 136 | + | print_cur_config(android_app); | |
| 137 | + | break; | |
| 138 | + | ||
| 139 | + | case APP_CMD_DESTROY: | |
| 140 | + | LOGV("APP_CMD_DESTROY\n"); | |
| 141 | + | android_app->destroyRequested = 1; | |
| 142 | + | break; | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) { | |
| 147 | + | switch (cmd) { | |
| 148 | + | case APP_CMD_TERM_WINDOW: | |
| 149 | + | LOGV("APP_CMD_TERM_WINDOW\n"); | |
| 150 | + | pthread_mutex_lock(&android_app->mutex); | |
| 151 | + | android_app->window = NULL; | |
| 152 | + | pthread_cond_broadcast(&android_app->cond); | |
| 153 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 154 | + | break; | |
| 155 | + | ||
| 156 | + | case APP_CMD_SAVE_STATE: | |
| 157 | + | LOGV("APP_CMD_SAVE_STATE\n"); | |
| 158 | + | pthread_mutex_lock(&android_app->mutex); | |
| 159 | + | android_app->stateSaved = 1; | |
| 160 | + | pthread_cond_broadcast(&android_app->cond); | |
| 161 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 162 | + | break; | |
| 163 | + | ||
| 164 | + | case APP_CMD_RESUME: | |
| 165 | + | free_saved_state(android_app); | |
| 166 | + | break; | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | static void android_app_destroy(struct android_app* android_app) { | |
| 171 | + | LOGV("android_app_destroy!"); | |
| 172 | + | free_saved_state(android_app); | |
| 173 | + | pthread_mutex_lock(&android_app->mutex); | |
| 174 | + | if (android_app->inputQueue != NULL) { | |
| 175 | + | AInputQueue_detachLooper(android_app->inputQueue); | |
| 176 | + | } | |
| 177 | + | AConfiguration_delete(android_app->config); | |
| 178 | + | android_app->destroyed = 1; | |
| 179 | + | pthread_cond_broadcast(&android_app->cond); | |
| 180 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 181 | + | // Can't touch android_app object after this. | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | static void process_input(struct android_app* app, struct android_poll_source* source) { | |
| 185 | + | AInputEvent* event = NULL; | |
| 186 | + | while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) { | |
| 187 | + | LOGV("New input event: type=%d\n", AInputEvent_getType(event)); | |
| 188 | + | if (AInputQueue_preDispatchEvent(app->inputQueue, event)) { | |
| 189 | + | continue; | |
| 190 | + | } | |
| 191 | + | int32_t handled = 0; | |
| 192 | + | if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event); | |
| 193 | + | AInputQueue_finishEvent(app->inputQueue, event, handled); | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | static void process_cmd(struct android_app* app, struct android_poll_source* source) { | |
| 198 | + | int8_t cmd = android_app_read_cmd(app); | |
| 199 | + | android_app_pre_exec_cmd(app, cmd); | |
| 200 | + | if (app->onAppCmd != NULL) app->onAppCmd(app, cmd); | |
| 201 | + | android_app_post_exec_cmd(app, cmd); | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | static void* android_app_entry(void* param) { | |
| 205 | + | struct android_app* android_app = (struct android_app*)param; | |
| 206 | + | ||
| 207 | + | android_app->config = AConfiguration_new(); | |
| 208 | + | AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager); | |
| 209 | + | ||
| 210 | + | print_cur_config(android_app); | |
| 211 | + | ||
| 212 | + | android_app->cmdPollSource.id = LOOPER_ID_MAIN; | |
| 213 | + | android_app->cmdPollSource.app = android_app; | |
| 214 | + | android_app->cmdPollSource.process = process_cmd; | |
| 215 | + | android_app->inputPollSource.id = LOOPER_ID_INPUT; | |
| 216 | + | android_app->inputPollSource.app = android_app; | |
| 217 | + | android_app->inputPollSource.process = process_input; | |
| 218 | + | ||
| 219 | + | ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); | |
| 220 | + | ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL, | |
| 221 | + | &android_app->cmdPollSource); | |
| 222 | + | android_app->looper = looper; | |
| 223 | + | ||
| 224 | + | pthread_mutex_lock(&android_app->mutex); | |
| 225 | + | android_app->running = 1; | |
| 226 | + | pthread_cond_broadcast(&android_app->cond); | |
| 227 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 228 | + | ||
| 229 | + | android_main(android_app); | |
| 230 | + | ||
| 231 | + | android_app_destroy(android_app); | |
| 232 | + | return NULL; | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // -------------------------------------------------------------------- | |
| 236 | + | // Native activity interaction (called from main thread) | |
| 237 | + | // -------------------------------------------------------------------- | |
| 238 | + | ||
| 239 | + | static struct android_app* android_app_create(ANativeActivity* activity, | |
| 240 | + | void* savedState, size_t savedStateSize) { | |
| 241 | + | struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app)); | |
| 242 | + | memset(android_app, 0, sizeof(struct android_app)); | |
| 243 | + | android_app->activity = activity; | |
| 244 | + | ||
| 245 | + | pthread_mutex_init(&android_app->mutex, NULL); | |
| 246 | + | pthread_cond_init(&android_app->cond, NULL); | |
| 247 | + | ||
| 248 | + | if (savedState != NULL) { | |
| 249 | + | android_app->savedState = malloc(savedStateSize); | |
| 250 | + | android_app->savedStateSize = savedStateSize; | |
| 251 | + | memcpy(android_app->savedState, savedState, savedStateSize); | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | int msgpipe[2]; | |
| 255 | + | if (pipe(msgpipe)) { | |
| 256 | + | LOGE("could not create pipe: %s", strerror(errno)); | |
| 257 | + | return NULL; | |
| 258 | + | } | |
| 259 | + | android_app->msgread = msgpipe[0]; | |
| 260 | + | android_app->msgwrite = msgpipe[1]; | |
| 261 | + | ||
| 262 | + | pthread_attr_t attr; | |
| 263 | + | pthread_attr_init(&attr); | |
| 264 | + | pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); | |
| 265 | + | pthread_create(&android_app->thread, &attr, android_app_entry, android_app); | |
| 266 | + | ||
| 267 | + | // Wait for thread to start. | |
| 268 | + | pthread_mutex_lock(&android_app->mutex); | |
| 269 | + | while (!android_app->running) { | |
| 270 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 271 | + | } | |
| 272 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 273 | + | ||
| 274 | + | return android_app; | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) { | |
| 278 | + | if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { | |
| 279 | + | LOGE("Failure writing android_app cmd: %s\n", strerror(errno)); | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) { | |
| 284 | + | pthread_mutex_lock(&android_app->mutex); | |
| 285 | + | android_app->pendingInputQueue = inputQueue; | |
| 286 | + | android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED); | |
| 287 | + | while (android_app->inputQueue != android_app->pendingInputQueue) { | |
| 288 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 289 | + | } | |
| 290 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) { | |
| 294 | + | pthread_mutex_lock(&android_app->mutex); | |
| 295 | + | if (android_app->pendingWindow != NULL) { | |
| 296 | + | android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW); | |
| 297 | + | } | |
| 298 | + | android_app->pendingWindow = window; | |
| 299 | + | if (window != NULL) { | |
| 300 | + | android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW); | |
| 301 | + | } | |
| 302 | + | while (android_app->window != android_app->pendingWindow) { | |
| 303 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 304 | + | } | |
| 305 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) { | |
| 309 | + | pthread_mutex_lock(&android_app->mutex); | |
| 310 | + | android_app_write_cmd(android_app, cmd); | |
| 311 | + | while (android_app->activityState != cmd) { | |
| 312 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 313 | + | } | |
| 314 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | static void android_app_free(struct android_app* android_app) { | |
| 318 | + | pthread_mutex_lock(&android_app->mutex); | |
| 319 | + | android_app_write_cmd(android_app, APP_CMD_DESTROY); | |
| 320 | + | while (!android_app->destroyed) { | |
| 321 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 322 | + | } | |
| 323 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 324 | + | ||
| 325 | + | close(android_app->msgread); | |
| 326 | + | close(android_app->msgwrite); | |
| 327 | + | pthread_cond_destroy(&android_app->cond); | |
| 328 | + | pthread_mutex_destroy(&android_app->mutex); | |
| 329 | + | free(android_app); | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | static void onDestroy(ANativeActivity* activity) { | |
| 333 | + | LOGV("Destroy: %p\n", activity); | |
| 334 | + | android_app_free((struct android_app*)activity->instance); | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | static void onStart(ANativeActivity* activity) { | |
| 338 | + | LOGV("Start: %p\n", activity); | |
| 339 | + | android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START); | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | static void onResume(ANativeActivity* activity) { | |
| 343 | + | LOGV("Resume: %p\n", activity); | |
| 344 | + | android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME); | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) { | |
| 348 | + | struct android_app* android_app = (struct android_app*)activity->instance; | |
| 349 | + | void* savedState = NULL; | |
| 350 | + | ||
| 351 | + | LOGV("SaveInstanceState: %p\n", activity); | |
| 352 | + | pthread_mutex_lock(&android_app->mutex); | |
| 353 | + | android_app->stateSaved = 0; | |
| 354 | + | android_app_write_cmd(android_app, APP_CMD_SAVE_STATE); | |
| 355 | + | while (!android_app->stateSaved) { | |
| 356 | + | pthread_cond_wait(&android_app->cond, &android_app->mutex); | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | if (android_app->savedState != NULL) { | |
| 360 | + | savedState = android_app->savedState; | |
| 361 | + | *outLen = android_app->savedStateSize; | |
| 362 | + | android_app->savedState = NULL; | |
| 363 | + | android_app->savedStateSize = 0; | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | pthread_mutex_unlock(&android_app->mutex); | |
| 367 | + | ||
| 368 | + | return savedState; | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | static void onPause(ANativeActivity* activity) { | |
| 372 | + | LOGV("Pause: %p\n", activity); | |
| 373 | + | android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE); | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | static void onStop(ANativeActivity* activity) { | |
| 377 | + | LOGV("Stop: %p\n", activity); | |
| 378 | + | android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP); | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | static void onConfigurationChanged(ANativeActivity* activity) { | |
| 382 | + | struct android_app* android_app = (struct android_app*)activity->instance; | |
| 383 | + | LOGV("ConfigurationChanged: %p\n", activity); | |
| 384 | + | android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED); | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | static void onLowMemory(ANativeActivity* activity) { | |
| 388 | + | struct android_app* android_app = (struct android_app*)activity->instance; | |
| 389 | + | LOGV("LowMemory: %p\n", activity); | |
| 390 | + | android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY); | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | static void onWindowFocusChanged(ANativeActivity* activity, int focused) { | |
| 394 | + | LOGV("WindowFocusChanged: %p -- %d\n", activity, focused); | |
| 395 | + | android_app_write_cmd((struct android_app*)activity->instance, | |
| 396 | + | focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS); | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) { | |
| 400 | + | LOGV("NativeWindowCreated: %p -- %p\n", activity, window); | |
| 401 | + | android_app_set_window((struct android_app*)activity->instance, window); | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) { | |
| 405 | + | LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window); | |
| 406 | + | android_app_set_window((struct android_app*)activity->instance, NULL); | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) { | |
| 410 | + | LOGV("InputQueueCreated: %p -- %p\n", activity, queue); | |
| 411 | + | android_app_set_input((struct android_app*)activity->instance, queue); | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) { | |
| 415 | + | LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue); | |
| 416 | + | android_app_set_input((struct android_app*)activity->instance, NULL); | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | void ANativeActivity_onCreate(ANativeActivity* activity, | |
| 420 | + | void* savedState, size_t savedStateSize) { | |
| 421 | + | LOGV("Creating: %p\n", activity); | |
| 422 | + | activity->callbacks->onDestroy = onDestroy; | |
| 423 | + | activity->callbacks->onStart = onStart; | |
| 424 | + | activity->callbacks->onResume = onResume; | |
| 425 | + | activity->callbacks->onSaveInstanceState = onSaveInstanceState; | |
| 426 | + | activity->callbacks->onPause = onPause; | |
| 427 | + | activity->callbacks->onStop = onStop; | |
| 428 | + | activity->callbacks->onConfigurationChanged = onConfigurationChanged; | |
| 429 | + | activity->callbacks->onLowMemory = onLowMemory; | |
| 430 | + | activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; | |
| 431 | + | activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; | |
| 432 | + | activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; | |
| 433 | + | activity->callbacks->onInputQueueCreated = onInputQueueCreated; | |
| 434 | + | activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed; | |
| 435 | + | ||
| 436 | + | activity->instance = android_app_create(activity, savedState, savedStateSize); | |
| 437 | + | } |
| @@ -0,0 +1,344 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (C) 2010 The Android Open Source Project | |
| 3 | + | * | |
| 4 | + | * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | + | * you may not use this file except in compliance with the License. | |
| 6 | + | * You may obtain a copy of the License at | |
| 7 | + | * | |
| 8 | + | * http://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | + | * | |
| 10 | + | * Unless required by applicable law or agreed to in writing, software | |
| 11 | + | * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | + | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | + | * See the License for the specific language governing permissions and | |
| 14 | + | * limitations under the License. | |
| 15 | + | * | |
| 16 | + | */ | |
| 17 | + | ||
| 18 | + | #ifndef _ANDROID_NATIVE_APP_GLUE_H | |
| 19 | + | #define _ANDROID_NATIVE_APP_GLUE_H | |
| 20 | + | ||
| 21 | + | #include <poll.h> | |
| 22 | + | #include <pthread.h> | |
| 23 | + | #include <sched.h> | |
| 24 | + | ||
| 25 | + | #include <android/configuration.h> | |
| 26 | + | #include <android/looper.h> | |
| 27 | + | #include <android/native_activity.h> | |
| 28 | + | ||
| 29 | + | //#ifdef __cplusplus | |
| 30 | + | //extern "C" { | |
| 31 | + | //#endif | |
| 32 | + | ||
| 33 | + | /** | |
| 34 | + | * The native activity interface provided by <android/native_activity.h> | |
| 35 | + | * is based on a set of application-provided callbacks that will be called | |
| 36 | + | * by the Activity's main thread when certain events occur. | |
| 37 | + | * | |
| 38 | + | * This means that each one of this callbacks _should_ _not_ block, or they | |
| 39 | + | * risk having the system force-close the application. This programming | |
| 40 | + | * model is direct, lightweight, but constraining. | |
| 41 | + | * | |
| 42 | + | * The 'threaded_native_app' static library is used to provide a different | |
| 43 | + | * execution model where the application can implement its own main event | |
| 44 | + | * loop in a different thread instead. Here's how it works: | |
| 45 | + | * | |
| 46 | + | * 1/ The application must provide a function named "android_main()" that | |
| 47 | + | * will be called when the activity is created, in a new thread that is | |
| 48 | + | * distinct from the activity's main thread. | |
| 49 | + | * | |
| 50 | + | * 2/ android_main() receives a pointer to a valid "android_app" structure | |
| 51 | + | * that contains references to other important objects, e.g. the | |
| 52 | + | * ANativeActivity obejct instance the application is running in. | |
| 53 | + | * | |
| 54 | + | * 3/ the "android_app" object holds an ALooper instance that already | |
| 55 | + | * listens to two important things: | |
| 56 | + | * | |
| 57 | + | * - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX | |
| 58 | + | * declarations below. | |
| 59 | + | * | |
| 60 | + | * - input events coming from the AInputQueue attached to the activity. | |
| 61 | + | * | |
| 62 | + | * Each of these correspond to an ALooper identifier returned by | |
| 63 | + | * ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT, | |
| 64 | + | * respectively. | |
| 65 | + | * | |
| 66 | + | * Your application can use the same ALooper to listen to additional | |
| 67 | + | * file-descriptors. They can either be callback based, or with return | |
| 68 | + | * identifiers starting with LOOPER_ID_USER. | |
| 69 | + | * | |
| 70 | + | * 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event, | |
| 71 | + | * the returned data will point to an android_poll_source structure. You | |
| 72 | + | * can call the process() function on it, and fill in android_app->onAppCmd | |
| 73 | + | * and android_app->onInputEvent to be called for your own processing | |
| 74 | + | * of the event. | |
| 75 | + | * | |
| 76 | + | * Alternatively, you can call the low-level functions to read and process | |
| 77 | + | * the data directly... look at the process_cmd() and process_input() | |
| 78 | + | * implementations in the glue to see how to do this. | |
| 79 | + | * | |
| 80 | + | * See the sample named "native-activity" that comes with the NDK with a | |
| 81 | + | * full usage example. Also look at the JavaDoc of NativeActivity. | |
| 82 | + | */ | |
| 83 | + | ||
| 84 | + | struct android_app; | |
| 85 | + | ||
| 86 | + | /** | |
| 87 | + | * Data associated with an ALooper fd that will be returned as the "outData" | |
| 88 | + | * when that source has data ready. | |
| 89 | + | */ | |
| 90 | + | struct android_poll_source { | |
| 91 | + | // The identifier of this source. May be LOOPER_ID_MAIN or | |
| 92 | + | // LOOPER_ID_INPUT. | |
| 93 | + | int32_t id; | |
| 94 | + | ||
| 95 | + | // The android_app this ident is associated with. | |
| 96 | + | struct android_app* app; | |
| 97 | + | ||
| 98 | + | // Function to call to perform the standard processing of data from | |
| 99 | + | // this source. | |
| 100 | + | void (*process)(struct android_app* app, struct android_poll_source* source); | |
| 101 | + | }; | |
| 102 | + | ||
| 103 | + | /** | |
| 104 | + | * This is the interface for the standard glue code of a threaded | |
| 105 | + | * application. In this model, the application's code is running | |
| 106 | + | * in its own thread separate from the main thread of the process. | |
| 107 | + | * It is not required that this thread be associated with the Java | |
| 108 | + | * VM, although it will need to be in order to make JNI calls any | |
| 109 | + | * Java objects. | |
| 110 | + | */ | |
| 111 | + | struct android_app { | |
| 112 | + | // The application can place a pointer to its own state object | |
| 113 | + | // here if it likes. | |
| 114 | + | void* userData; | |
| 115 | + | ||
| 116 | + | // Fill this in with the function to process main app commands (APP_CMD_*) | |
| 117 | + | void (*onAppCmd)(struct android_app* app, int32_t cmd); | |
| 118 | + | ||
| 119 | + | // Fill this in with the function to process input events. At this point | |
| 120 | + | // the event has already been pre-dispatched, and it will be finished upon | |
| 121 | + | // return. Return 1 if you have handled the event, 0 for any default | |
| 122 | + | // dispatching. | |
| 123 | + | int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event); | |
| 124 | + | ||
| 125 | + | // The ANativeActivity object instance that this app is running in. | |
| 126 | + | ANativeActivity* activity; | |
| 127 | + | ||
| 128 | + | // The current configuration the app is running in. | |
| 129 | + | AConfiguration* config; | |
| 130 | + | ||
| 131 | + | // This is the last instance's saved state, as provided at creation time. | |
| 132 | + | // It is NULL if there was no state. You can use this as you need; the | |
| 133 | + | // memory will remain around until you call android_app_exec_cmd() for | |
| 134 | + | // APP_CMD_RESUME, at which point it will be freed and savedState set to NULL. | |
| 135 | + | // These variables should only be changed when processing a APP_CMD_SAVE_STATE, | |
| 136 | + | // at which point they will be initialized to NULL and you can malloc your | |
| 137 | + | // state and place the information here. In that case the memory will be | |
| 138 | + | // freed for you later. | |
| 139 | + | void* savedState; | |
| 140 | + | size_t savedStateSize; | |
| 141 | + | ||
| 142 | + | // The ALooper associated with the app's thread. | |
| 143 | + | ALooper* looper; | |
| 144 | + | ||
| 145 | + | // When non-NULL, this is the input queue from which the app will | |
| 146 | + | // receive user input events. | |
| 147 | + | AInputQueue* inputQueue; | |
| 148 | + | ||
| 149 | + | // When non-NULL, this is the window surface that the app can draw in. | |
| 150 | + | ANativeWindow* window; | |
| 151 | + | ||
| 152 | + | // Current content rectangle of the window; this is the area where the | |
| 153 | + | // window's content should be placed to be seen by the user. | |
| 154 | + | ARect contentRect; | |
| 155 | + | ||
| 156 | + | // Current state of the app's activity. May be either APP_CMD_START, | |
| 157 | + | // APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below. | |
| 158 | + | int activityState; | |
| 159 | + | ||
| 160 | + | // This is non-zero when the application's NativeActivity is being | |
| 161 | + | // destroyed and waiting for the app thread to complete. | |
| 162 | + | int destroyRequested; | |
| 163 | + | ||
| 164 | + | // ------------------------------------------------- | |
| 165 | + | // Below are "private" implementation of the glue code. | |
| 166 | + | ||
| 167 | + | pthread_mutex_t mutex; | |
| 168 | + | pthread_cond_t cond; | |
| 169 | + | ||
| 170 | + | int msgread; | |
| 171 | + | int msgwrite; | |
| 172 | + | ||
| 173 | + | pthread_t thread; | |
| 174 | + | ||
| 175 | + | struct android_poll_source cmdPollSource; | |
| 176 | + | struct android_poll_source inputPollSource; | |
| 177 | + | ||
| 178 | + | int running; | |
| 179 | + | int stateSaved; | |
| 180 | + | int destroyed; | |
| 181 | + | int redrawNeeded; | |
| 182 | + | AInputQueue* pendingInputQueue; | |
| 183 | + | ANativeWindow* pendingWindow; | |
| 184 | + | ARect pendingContentRect; | |
| 185 | + | }; | |
| 186 | + | ||
| 187 | + | enum { | |
| 188 | + | /** | |
| 189 | + | * Looper data ID of commands coming from the app's main thread, which | |
| 190 | + | * is returned as an identifier from ALooper_pollOnce(). The data for this | |
| 191 | + | * identifier is a pointer to an android_poll_source structure. | |
| 192 | + | * These can be retrieved and processed with android_app_read_cmd() | |
| 193 | + | * and android_app_exec_cmd(). | |
| 194 | + | */ | |
| 195 | + | LOOPER_ID_MAIN = 1, | |
| 196 | + | ||
| 197 | + | /** | |
| 198 | + | * Looper data ID of events coming from the AInputQueue of the | |
| 199 | + | * application's window, which is returned as an identifier from | |
| 200 | + | * ALooper_pollOnce(). The data for this identifier is a pointer to an | |
| 201 | + | * android_poll_source structure. These can be read via the inputQueue | |
| 202 | + | * object of android_app. | |
| 203 | + | */ | |
| 204 | + | LOOPER_ID_INPUT = 2, | |
| 205 | + | ||
| 206 | + | /** | |
| 207 | + | * Start of user-defined ALooper identifiers. | |
| 208 | + | */ | |
| 209 | + | LOOPER_ID_USER = 3, | |
| 210 | + | }; | |
| 211 | + | ||
| 212 | + | enum { | |
| 213 | + | /** | |
| 214 | + | * Command from main thread: the AInputQueue has changed. Upon processing | |
| 215 | + | * this command, android_app->inputQueue will be updated to the new queue | |
| 216 | + | * (or NULL). | |
| 217 | + | */ | |
| 218 | + | APP_CMD_INPUT_CHANGED, | |
| 219 | + | ||
| 220 | + | /** | |
| 221 | + | * Command from main thread: a new ANativeWindow is ready for use. Upon | |
| 222 | + | * receiving this command, android_app->window will contain the new window | |
| 223 | + | * surface. | |
| 224 | + | */ | |
| 225 | + | APP_CMD_INIT_WINDOW, | |
| 226 | + | ||
| 227 | + | /** | |
| 228 | + | * Command from main thread: the existing ANativeWindow needs to be | |
| 229 | + | * terminated. Upon receiving this command, android_app->window still | |
| 230 | + | * contains the existing window; after calling android_app_exec_cmd | |
| 231 | + | * it will be set to NULL. | |
| 232 | + | */ | |
| 233 | + | APP_CMD_TERM_WINDOW, | |
| 234 | + | ||
| 235 | + | /** | |
| 236 | + | * Command from main thread: the current ANativeWindow has been resized. | |
| 237 | + | * Please redraw with its new size. | |
| 238 | + | */ | |
| 239 | + | APP_CMD_WINDOW_RESIZED, | |
| 240 | + | ||
| 241 | + | /** | |
| 242 | + | * Command from main thread: the system needs that the current ANativeWindow | |
| 243 | + | * be redrawn. You should redraw the window before handing this to | |
| 244 | + | * android_app_exec_cmd() in order to avoid transient drawing glitches. | |
| 245 | + | */ | |
| 246 | + | APP_CMD_WINDOW_REDRAW_NEEDED, | |
| 247 | + | ||
| 248 | + | /** | |
| 249 | + | * Command from main thread: the content area of the window has changed, | |
| 250 | + | * such as from the soft input window being shown or hidden. You can | |
| 251 | + | * find the new content rect in android_app::contentRect. | |
| 252 | + | */ | |
| 253 | + | APP_CMD_CONTENT_RECT_CHANGED, | |
| 254 | + | ||
| 255 | + | /** | |
| 256 | + | * Command from main thread: the app's activity window has gained | |
| 257 | + | * input focus. | |
| 258 | + | */ | |
| 259 | + | APP_CMD_GAINED_FOCUS, | |
| 260 | + | ||
| 261 | + | /** | |
| 262 | + | * Command from main thread: the app's activity window has lost | |
| 263 | + | * input focus. | |
| 264 | + | */ | |
| 265 | + | APP_CMD_LOST_FOCUS, | |
| 266 | + | ||
| 267 | + | /** | |
| 268 | + | * Command from main thread: the current device configuration has changed. | |
| 269 | + | */ | |
| 270 | + | APP_CMD_CONFIG_CHANGED, | |
| 271 | + | ||
| 272 | + | /** | |
| 273 | + | * Command from main thread: the system is running low on memory. | |
| 274 | + | * Try to reduce your memory use. | |
| 275 | + | */ | |
| 276 | + | APP_CMD_LOW_MEMORY, | |
| 277 | + | ||
| 278 | + | /** | |
| 279 | + | * Command from main thread: the app's activity has been started. | |
| 280 | + | */ | |
| 281 | + | APP_CMD_START, | |
| 282 | + | ||
| 283 | + | /** | |
| 284 | + | * Command from main thread: the app's activity has been resumed. | |
| 285 | + | */ | |
| 286 | + | APP_CMD_RESUME, | |
| 287 | + | ||
| 288 | + | /** | |
| 289 | + | * Command from main thread: the app should generate a new saved state | |
| 290 | + | * for itself, to restore from later if needed. If you have saved state, | |
| 291 | + | * allocate it with malloc and place it in android_app.savedState with | |
| 292 | + | * the size in android_app.savedStateSize. The will be freed for you | |
| 293 | + | * later. | |
| 294 | + | */ | |
| 295 | + | APP_CMD_SAVE_STATE, | |
| 296 | + | ||
| 297 | + | /** | |
| 298 | + | * Command from main thread: the app's activity has been paused. | |
| 299 | + | */ | |
| 300 | + | APP_CMD_PAUSE, | |
| 301 | + | ||
| 302 | + | /** | |
| 303 | + | * Command from main thread: the app's activity has been stopped. | |
| 304 | + | */ | |
| 305 | + | APP_CMD_STOP, | |
| 306 | + | ||
| 307 | + | /** | |
| 308 | + | * Command from main thread: the app's activity is being destroyed, | |
| 309 | + | * and waiting for the app thread to clean up and exit before proceeding. | |
| 310 | + | */ | |
| 311 | + | APP_CMD_DESTROY, | |
| 312 | + | }; | |
| 313 | + | ||
| 314 | + | /** | |
| 315 | + | * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next | |
| 316 | + | * app command message. | |
| 317 | + | */ | |
| 318 | + | int8_t android_app_read_cmd(struct android_app* android_app); | |
| 319 | + | ||
| 320 | + | /** | |
| 321 | + | * Call with the command returned by android_app_read_cmd() to do the | |
| 322 | + | * initial pre-processing of the given command. You can perform your own | |
| 323 | + | * actions for the command after calling this function. | |
| 324 | + | */ | |
| 325 | + | void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd); | |
| 326 | + | ||
| 327 | + | /** | |
| 328 | + | * Call with the command returned by android_app_read_cmd() to do the | |
| 329 | + | * final post-processing of the given command. You must have done your own | |
| 330 | + | * actions for the command before calling this function. | |
| 331 | + | */ | |
| 332 | + | void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd); | |
| 333 | + | ||
| 334 | + | /** | |
| 335 | + | * This is the function that application code must implement, representing | |
| 336 | + | * the main entry to the app. | |
| 337 | + | */ | |
| 338 | + | extern void android_main(struct android_app* app); | |
| 339 | + | ||
| 340 | + | //#ifdef __cplusplus | |
| 341 | + | //} | |
| 342 | + | //#endif | |
| 343 | + | ||
| 344 | + | #endif /* _ANDROID_NATIVE_APP_GLUE_H */ |
| @@ -0,0 +1,29 @@ | |||
| 1 | + | #if (__ANDROID_API__ > 19) | |
| 2 | + | #include <android/api-level.h> | |
| 3 | + | #include <android/log.h> | |
| 4 | + | #include <signal.h> | |
| 5 | + | #include <dlfcn.h> | |
| 6 | + | ||
| 7 | + | extern "C" { | |
| 8 | + | typedef __sighandler_t(*bsd_signal_func_t)(int, __sighandler_t); | |
| 9 | + | bsd_signal_func_t bsd_signal_func = NULL; | |
| 10 | + | ||
| 11 | + | __sighandler_t bsd_signal(int s, __sighandler_t f) | |
| 12 | + | { | |
| 13 | + | if (bsd_signal_func == NULL) { | |
| 14 | + | // For now (up to Android 7.0) this is always available | |
| 15 | + | bsd_signal_func = (bsd_signal_func_t)dlsym(RTLD_DEFAULT, "bsd_signal"); | |
| 16 | + | ||
| 17 | + | if (bsd_signal_func == NULL) { | |
| 18 | + | // You may try dlsym(RTLD_DEFAULT, "signal") or dlsym(RTLD_NEXT, "signal") here | |
| 19 | + | // Make sure you add a comment here in StackOverflow | |
| 20 | + | // if you find a device that doesn't have "bsd_signal" in its libc.so!!! | |
| 21 | + | ||
| 22 | + | __android_log_assert("", "bsd_signal_wrapper", "bsd_signal symbol not found!"); | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | return bsd_signal_func(s, f); | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | #endif |
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // | |
| 2 | + | // pch.h | |
| 3 | + | // Header for standard system include files. | |
| 4 | + | // | |
| 5 | + | // Used by the build system to generate the precompiled header. Note that no | |
| 6 | + | // pch.cpp is needed and the pch.h is automatically included in all cpp files | |
| 7 | + | // that are part of the project | |
| 8 | + | // | |
| 9 | + | ||
| 10 | + | #include <jni.h> | |
| 11 | + | #include <errno.h> | |
| 12 | + | ||
| 13 | + | #include <string.h> | |
| 14 | + | #include <unistd.h> | |
| 15 | + | #include <sys/resource.h> | |
| 16 | + | ||
| 17 | + | #include <EGL/egl.h> | |
| 18 | + | #include <GLES/gl.h> | |
| 19 | + | ||
| 20 | + | #include <android/sensor.h> | |
| 21 | + | ||
| 22 | + | #include <android/log.h> | |
| 23 | + | #include <android/native_window.h> | |
| 24 | + | #include <android/native_window_jni.h> | |
| 25 | + | ||
| 26 | + | #include "android_native_app_glue.h" | |
| 27 | + | ||
| 28 | + | extern android_app* g_androidState; |
| @@ -0,0 +1,94 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "lightview.h" | |
| 24 | + | #include "spritemanager.h" | |
| 25 | + | #include <framework/graphics/painter.h> | |
| 26 | + | ||
| 27 | + | void LightView::addLight(const Point& pos, uint8_t color, uint8_t intensity) | |
| 28 | + | { | |
| 29 | + | if (!m_lights.empty()) { | |
| 30 | + | Light& prevLight = m_lights.back(); | |
| 31 | + | if (prevLight.pos == pos && prevLight.color == color) { | |
| 32 | + | prevLight.intensity = std::max(prevLight.intensity, intensity); | |
| 33 | + | return; | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | m_lights.push_back(Light{ pos, color, intensity }); | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | void LightView::setFieldBrightness(const Point& pos, size_t start, uint8_t color) | |
| 40 | + | { | |
| 41 | + | size_t index = (pos.y / g_sprites.spriteSize()) * m_mapSize.width() + (pos.x / g_sprites.spriteSize()); | |
| 42 | + | if (index >= m_tiles.size()) return; | |
| 43 | + | m_tiles[index].start = start; | |
| 44 | + | m_tiles[index].color = color; | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | void LightView::draw() // render thread | |
| 48 | + | { | |
| 49 | + | // TODO: optimize in the future for big areas | |
| 50 | + | static std::vector<uint8_t> buffer; | |
| 51 | + | if (buffer.size() < 4u * m_mapSize.area()) | |
| 52 | + | buffer.resize(m_mapSize.area() * 4); | |
| 53 | + | ||
| 54 | + | for (int x = 0; x < m_mapSize.width(); ++x) { | |
| 55 | + | for (int y = 0; y < m_mapSize.height(); ++y) { | |
| 56 | + | Point pos(x * g_sprites.spriteSize() + g_sprites.spriteSize() / 2, y * g_sprites.spriteSize() + g_sprites.spriteSize() / 2); | |
| 57 | + | int index = (y * m_mapSize.width() + x); | |
| 58 | + | int colorIndex = index * 4; | |
| 59 | + | buffer[colorIndex] = m_globalLight.r(); | |
| 60 | + | buffer[colorIndex + 1] = m_globalLight.g(); | |
| 61 | + | buffer[colorIndex + 2] = m_globalLight.b(); | |
| 62 | + | buffer[colorIndex + 3] = 255; // alpha channel | |
| 63 | + | for (size_t i = m_tiles[index].start; i < m_lights.size(); ++i) { | |
| 64 | + | Light& light = m_lights[i]; | |
| 65 | + | float distance = std::sqrt((pos.x - light.pos.x) * (pos.x - light.pos.x) + | |
| 66 | + | (pos.y - light.pos.y) * (pos.y - light.pos.y)); | |
| 67 | + | distance /= g_sprites.spriteSize(); | |
| 68 | + | float intensity = (-distance + light.intensity) * 0.2f; | |
| 69 | + | if (intensity < 0.01f) continue; | |
| 70 | + | if (intensity > 1.0f) intensity = 1.0f; | |
| 71 | + | Color lightColor = Color::from8bit(light.color) * intensity; | |
| 72 | + | buffer[colorIndex] = std::max<int>(buffer[colorIndex], lightColor.r()); | |
| 73 | + | buffer[colorIndex + 1] = std::max<int>(buffer[colorIndex + 1], lightColor.g()); | |
| 74 | + | buffer[colorIndex + 2] = std::max<int>(buffer[colorIndex + 2], lightColor.b()); | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | m_lightTexture->update(); | |
| 80 | + | glBindTexture(GL_TEXTURE_2D, m_lightTexture->getId()); | |
| 81 | + | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_mapSize.width(), m_mapSize.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer.data()); | |
| 82 | + | ||
| 83 | + | Point offset = m_src.topLeft(); | |
| 84 | + | Size size = m_src.size(); | |
| 85 | + | CoordsBuffer coords; | |
| 86 | + | coords.addRect(RectF(m_dest.left(), m_dest.top(), m_dest.width(), m_dest.height()), | |
| 87 | + | RectF((float)offset.x / g_sprites.spriteSize(), (float)offset.y / g_sprites.spriteSize(), | |
| 88 | + | (float)size.width() / g_sprites.spriteSize(), (float)size.height() / g_sprites.spriteSize())); | |
| 89 | + | ||
| 90 | + | g_painter->resetColor(); | |
| 91 | + | g_painter->setCompositionMode(Painter::CompositionMode_Multiply); | |
| 92 | + | g_painter->drawTextureCoords(coords, m_lightTexture); | |
| 93 | + | g_painter->resetCompositionMode(); | |
| 94 | + | } |
| @@ -0,0 +1,714 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "mapview.h" | |
| 24 | + | ||
| 25 | + | #include "animatedtext.h" | |
| 26 | + | #include "creature.h" | |
| 27 | + | #include "game.h" | |
| 28 | + | #include "lightview.h" | |
| 29 | + | #include "localplayer.h" | |
| 30 | + | #include "map.h" | |
| 31 | + | #include "missile.h" | |
| 32 | + | #include "spritemanager.h" | |
| 33 | + | #include "statictext.h" | |
| 34 | + | #include "tile.h" | |
| 35 | + | ||
| 36 | + | #include <framework/core/application.h> | |
| 37 | + | #include <framework/core/eventdispatcher.h> | |
| 38 | + | #include <framework/core/resourcemanager.h> | |
| 39 | + | #include <framework/graphics/atlas.h> | |
| 40 | + | #include <framework/graphics/framebuffermanager.h> | |
| 41 | + | #include <framework/graphics/graphics.h> | |
| 42 | + | #include <framework/graphics/image.h> | |
| 43 | + | #include <framework/graphics/shadermanager.h> | |
| 44 | + | #include <framework/graphics/texturemanager.h> | |
| 45 | + | ||
| 46 | + | #include <framework/core/adaptiverenderer.h> | |
| 47 | + | #include <framework/util/extras.h> | |
| 48 | + | ||
| 49 | + | MapView::MapView() { | |
| 50 | + | m_lockedFirstVisibleFloor = -1; | |
| 51 | + | m_cachedFirstVisibleFloor = 7; | |
| 52 | + | m_cachedLastVisibleFloor = 7; | |
| 53 | + | m_minimumAmbientLight = 0; | |
| 54 | + | m_optimizedSize = Size(g_map.getAwareRange().horizontal(), | |
| 55 | + | g_map.getAwareRange().vertical()) * | |
| 56 | + | g_sprites.spriteSize(); | |
| 57 | + | ||
| 58 | + | setVisibleDimension(Size(15, 11)); | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | MapView::~MapView() { VALIDATE(!g_app.isTerminated()); } | |
| 62 | + | ||
| 63 | + | void MapView::drawTileTexts(const Rect &rect, const Rect &srcRect) { | |
| 64 | + | Position cameraPosition = getCameraPosition(); | |
| 65 | + | Point drawOffset = srcRect.topLeft(); | |
| 66 | + | float horizontalStretchFactor = rect.width() / (float)srcRect.width(); | |
| 67 | + | float verticalStretchFactor = rect.height() / (float)srcRect.height(); | |
| 68 | + | ||
| 69 | + | auto player = g_game.getLocalPlayer(); | |
| 70 | + | auto floor = player->getPosition().z; | |
| 71 | + | for (auto &tile : m_cachedVisibleTiles[floor]) { | |
| 72 | + | Position tilePos = tile->getPosition(); | |
| 73 | + | Point p = transformPositionTo2D(tilePos, cameraPosition) - drawOffset; | |
| 74 | + | p.x *= horizontalStretchFactor; | |
| 75 | + | p.y *= verticalStretchFactor; | |
| 76 | + | p += rect.topLeft(); | |
| 77 | + | p.y += 5; | |
| 78 | + | ||
| 79 | + | tile->drawTexts(p); | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | void MapView::drawTileWidget(const Rect &rect, const Rect &srcRect) { | |
| 84 | + | Position cameraPosition = getCameraPosition(); | |
| 85 | + | Point drawOffset = srcRect.topLeft(); | |
| 86 | + | float horizontalStretchFactor = rect.width() / (float)srcRect.width(); | |
| 87 | + | float verticalStretchFactor = rect.height() / (float)srcRect.height(); | |
| 88 | + | ||
| 89 | + | auto player = g_game.getLocalPlayer(); | |
| 90 | + | auto floor = player->getPosition().z; | |
| 91 | + | for (auto &tile : m_cachedVisibleTiles[floor]) { | |
| 92 | + | Position tilePos = tile->getPosition(); | |
| 93 | + | if (tilePos.z != player->getPosition().z) | |
| 94 | + | continue; | |
| 95 | + | ||
| 96 | + | Point p = transformPositionTo2D(tilePos, cameraPosition) - drawOffset; | |
| 97 | + | p.x *= horizontalStretchFactor; | |
| 98 | + | p.y *= verticalStretchFactor; | |
| 99 | + | p += rect.topLeft(); | |
| 100 | + | ||
| 101 | + | size_t drawQueueStart = g_drawQueue->size(); | |
| 102 | + | tile->drawWidget(p); | |
| 103 | + | g_drawQueue->setClip(drawQueueStart, rect); | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | void MapView::drawMapBackground(const Rect &rect, | |
| 108 | + | const TilePtr &crosshairTile) { | |
| 109 | + | Position cameraPosition = getCameraPosition(); | |
| 110 | + | if (m_mustUpdateVisibleTilesCache) { | |
| 111 | + | updateVisibleTilesCache(); | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | if (g_game.getFeature(Otc::GameForceLight)) { | |
| 115 | + | m_drawLight = true; | |
| 116 | + | m_minimumAmbientLight = 0.05f; | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | Rect srcRect = calcFramebufferSource(rect.size()); | |
| 120 | + | g_drawQueue->setFrameBuffer(rect, m_optimizedSize, srcRect); | |
| 121 | + | ||
| 122 | + | if (m_drawLight) { | |
| 123 | + | Light ambientLight; | |
| 124 | + | if (cameraPosition.z <= Otc::SEA_FLOOR) | |
| 125 | + | ambientLight = g_map.getLight(); | |
| 126 | + | if (!m_lightTexture || m_lightTexture->getSize() != m_drawDimension) | |
| 127 | + | m_lightTexture = TexturePtr(new Texture(m_drawDimension, false, true)); | |
| 128 | + | m_lightView = std::make_unique<LightView>( | |
| 129 | + | m_lightTexture, m_drawDimension, rect, srcRect, ambientLight.color, | |
| 130 | + | std::max<int>(m_minimumAmbientLight * 255, ambientLight.intensity)); | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | for (int z = m_cachedLastVisibleFloor; z >= m_cachedFirstFadingFloor; --z) { | |
| 134 | + | float fading = 1.0; | |
| 135 | + | if (m_floorFading > 0) { | |
| 136 | + | fading = 0.; | |
| 137 | + | if (m_floorFading > 0) { | |
| 138 | + | fading = stdext::clamp<float>( | |
| 139 | + | (float)m_fadingFloorTimers[z].elapsed_millis() / | |
| 140 | + | (float)m_floorFading, | |
| 141 | + | 0.f, 1.f); | |
| 142 | + | if (z < m_cachedFirstVisibleFloor) | |
| 143 | + | fading = 1.0 - fading; | |
| 144 | + | } | |
| 145 | + | if (fading == 0) | |
| 146 | + | break; | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | size_t floorStart = g_drawQueue->size(); | |
| 150 | + | drawFloor(z, cameraPosition, crosshairTile); | |
| 151 | + | ||
| 152 | + | if (fading < 0.99) | |
| 153 | + | g_drawQueue->setOpacity(floorStart, fading); | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | void MapView::drawFloor(short floor, const Position &cameraPosition, | |
| 158 | + | const TilePtr &crosshairTile) { | |
| 159 | + | if (floor < 0 || floor > Otc::MAX_Z) | |
| 160 | + | return; | |
| 161 | + | ||
| 162 | + | auto &tiles = m_cachedVisibleTiles[floor]; | |
| 163 | + | size_t lightFloorStart = m_lightView ? m_lightView->size() : 0; | |
| 164 | + | ||
| 165 | + | // light | |
| 166 | + | if (m_lightView) { | |
| 167 | + | for (auto &tile : tiles) { | |
| 168 | + | Point tileDrawPos = | |
| 169 | + | transformPositionTo2D(tile->getPosition(), cameraPosition); | |
| 170 | + | ItemPtr ground = tile->getGround(); | |
| 171 | + | // FIX: Only block light for ground tiles ON the camera floor (z=7). | |
| 172 | + | // For floor 8 tiles rendered under floor 7 (through holes), do NOT | |
| 173 | + | // block their light, otherwise they render completely black. | |
| 174 | + | if (ground && ground->isGround() && !ground->isTranslucent() && | |
| 175 | + | tile->getPosition().z == cameraPosition.z) { | |
| 176 | + | m_lightView->setFieldBrightness(tileDrawPos, lightFloorStart, 0); | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | if (g_game.getFeature(Otc::GameMapDrawGroundFirst)) { | |
| 182 | + | // ground | |
| 183 | + | for (auto &tile : tiles) { | |
| 184 | + | Point tileDrawPos = | |
| 185 | + | transformPositionTo2D(tile->getPosition(), cameraPosition); | |
| 186 | + | tile->drawGround(tileDrawPos, m_lightView.get()); | |
| 187 | + | } | |
| 188 | + | // bottom, creatures, top | |
| 189 | + | for (auto &tile : tiles) { | |
| 190 | + | Point tileDrawPos = | |
| 191 | + | transformPositionTo2D(tile->getPosition(), cameraPosition); | |
| 192 | + | ||
| 193 | + | tile->drawBottom(tileDrawPos, m_lightView.get()); | |
| 194 | + | ||
| 195 | + | if (m_crosshair && tile == crosshairTile) { | |
| 196 | + | g_drawQueue->addTexturedRect( | |
| 197 | + | Rect(tileDrawPos, tileDrawPos + g_sprites.spriteSize() - 1), | |
| 198 | + | m_crosshair, Rect(0, 0, m_crosshair->getSize())); | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | tile->drawCreatures(tileDrawPos, m_lightView.get()); | |
| 202 | + | tile->drawTop(tileDrawPos, m_lightView.get()); | |
| 203 | + | } | |
| 204 | + | } else { | |
| 205 | + | // ground, bottom, creatures, top | |
| 206 | + | for (auto &tile : tiles) { | |
| 207 | + | Point tileDrawPos = | |
| 208 | + | transformPositionTo2D(tile->getPosition(), cameraPosition); | |
| 209 | + | ||
| 210 | + | if (m_lightView) { | |
| 211 | + | ItemPtr ground = tile->getGround(); | |
| 212 | + | // FIX: Only block light on the camera's own floor, not on lower | |
| 213 | + | // floors (like floor 8 seen through floor 7 holes). | |
| 214 | + | if (ground && ground->isGround() && !ground->isTranslucent() && | |
| 215 | + | tile->getPosition().z == cameraPosition.z) { | |
| 216 | + | m_lightView->setFieldBrightness(tileDrawPos, lightFloorStart, 0); | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | tile->drawGround(tileDrawPos, m_lightView.get()); | |
| 221 | + | ||
| 222 | + | tile->drawBottom(tileDrawPos, m_lightView.get()); | |
| 223 | + | ||
| 224 | + | if (m_crosshair && tile == crosshairTile) { | |
| 225 | + | g_drawQueue->addTexturedRect( | |
| 226 | + | Rect(tileDrawPos, tileDrawPos + g_sprites.spriteSize() - 1), | |
| 227 | + | m_crosshair, Rect(0, 0, m_crosshair->getSize())); | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | tile->drawCreatures(tileDrawPos, m_lightView.get()); | |
| 231 | + | tile->drawTop(tileDrawPos, m_lightView.get()); | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | for (const MissilePtr &missile : g_map.getFloorMissiles(floor)) { | |
| 236 | + | missile->draw(transformPositionTo2D(missile->getPosition(), cameraPosition), | |
| 237 | + | true, m_lightView.get()); | |
| 238 | + | } | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | void MapView::drawMapForeground(const Rect &rect) { | |
| 242 | + | // this could happen if the player position is not known yet | |
| 243 | + | Position cameraPosition = getCameraPosition(); | |
| 244 | + | if (!cameraPosition.isValid()) | |
| 245 | + | return; | |
| 246 | + | ||
| 247 | + | Rect srcRect = calcFramebufferSource(rect.size()); | |
| 248 | + | Point drawOffset = srcRect.topLeft(); | |
| 249 | + | float horizontalStretchFactor = rect.width() / (float)srcRect.width(); | |
| 250 | + | float verticalStretchFactor = rect.height() / (float)srcRect.height(); | |
| 251 | + | ||
| 252 | + | // creatures | |
| 253 | + | std::vector<std::pair<CreaturePtr, Point>> creatures; | |
| 254 | + | for (const CreaturePtr &creature : g_map.getSpectatorsInRangeEx( | |
| 255 | + | cameraPosition, false, m_visibleDimension.width() / 2, | |
| 256 | + | m_visibleDimension.width() / 2 + 1, m_visibleDimension.height() / 2, | |
| 257 | + | m_visibleDimension.height() / 2 + 1)) { | |
| 258 | + | if (!creature->canBeSeen()) | |
| 259 | + | continue; | |
| 260 | + | ||
| 261 | + | PointF jumpOffset = creature->getJumpOffset(); | |
| 262 | + | Point creatureOffset = | |
| 263 | + | Point(16 * g_sprites.getOffsetFactor() - creature->getDisplacementX(), | |
| 264 | + | -creature->getDisplacementY() - 2 * g_sprites.getOffsetFactor()); | |
| 265 | + | Position pos = creature->getPrewalkingPosition(); | |
| 266 | + | Point p = transformPositionTo2D(pos, cameraPosition) - drawOffset; | |
| 267 | + | p += (creature->getDrawOffset() + creatureOffset) - | |
| 268 | + | Point(jumpOffset.x, jumpOffset.y); | |
| 269 | + | p.x = p.x * horizontalStretchFactor; | |
| 270 | + | p.y = p.y * verticalStretchFactor; | |
| 271 | + | p += rect.topLeft(); | |
| 272 | + | creatures.push_back(std::make_pair(creature, p)); | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | for (auto &c : creatures) { | |
| 276 | + | int flags = Otc::DrawIcons; | |
| 277 | + | if (m_drawNames) { | |
| 278 | + | flags |= Otc::DrawNames; | |
| 279 | + | } | |
| 280 | + | if ((!c.first->isLocalPlayer() || m_drawPlayerBars) && | |
| 281 | + | !m_drawHealthBarsOnTop) { | |
| 282 | + | if (m_drawHealthBars) { | |
| 283 | + | flags |= Otc::DrawBars; | |
| 284 | + | } | |
| 285 | + | if (m_drawManaBar) { | |
| 286 | + | flags |= Otc::DrawManaBar; | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | c.first->drawInformation(c.second, | |
| 290 | + | g_map.isCovered(c.first->getPrewalkingPosition(), | |
| 291 | + | m_cachedFirstVisibleFloor), | |
| 292 | + | rect, flags); | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | if (m_lightView) { | |
| 296 | + | g_drawQueue->add(m_lightView.release()); | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | // texts | |
| 300 | + | int limit = g_adaptiveRenderer.textsLimit(); | |
| 301 | + | for (int i = 0; i < 2; ++i) { | |
| 302 | + | for (const StaticTextPtr &staticText : g_map.getStaticTexts()) { | |
| 303 | + | Position pos = staticText->getPosition(); | |
| 304 | + | ||
| 305 | + | if (pos.z != cameraPosition.z && | |
| 306 | + | staticText->getMessageMode() == Otc::MessageNone) | |
| 307 | + | continue; | |
| 308 | + | if ((staticText->getMessageMode() != Otc::MessageSay && | |
| 309 | + | staticText->getMessageMode() != Otc::MessageYell)) { | |
| 310 | + | if (i == 0) | |
| 311 | + | continue; | |
| 312 | + | } else if (i == 1) | |
| 313 | + | continue; | |
| 314 | + | ||
| 315 | + | Point p = transformPositionTo2D(pos, cameraPosition) - drawOffset + | |
| 316 | + | Point(8, 0) * g_sprites.getOffsetFactor(); | |
| 317 | + | p.x *= horizontalStretchFactor; | |
| 318 | + | p.y *= verticalStretchFactor; | |
| 319 | + | p += rect.topLeft(); | |
| 320 | + | staticText->drawText(p, rect); | |
| 321 | + | if (--limit == 0) | |
| 322 | + | break; | |
| 323 | + | } | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | limit = g_adaptiveRenderer.textsLimit(); | |
| 327 | + | for (const AnimatedTextPtr &animatedText : g_map.getAnimatedTexts()) { | |
| 328 | + | Position pos = animatedText->getPosition(); | |
| 329 | + | ||
| 330 | + | if (pos.z != cameraPosition.z) | |
| 331 | + | continue; | |
| 332 | + | ||
| 333 | + | Point p = transformPositionTo2D(pos, cameraPosition) - drawOffset + | |
| 334 | + | Point(16, 8) * g_sprites.getOffsetFactor(); | |
| 335 | + | p.x *= horizontalStretchFactor; | |
| 336 | + | p.y *= verticalStretchFactor; | |
| 337 | + | p += rect.topLeft(); | |
| 338 | + | animatedText->drawText(p, rect); | |
| 339 | + | if (--limit == 0) | |
| 340 | + | break; | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | // tile texts | |
| 344 | + | drawTileTexts(rect, srcRect); | |
| 345 | + | ||
| 346 | + | // bars on top | |
| 347 | + | if (m_drawHealthBarsOnTop) { | |
| 348 | + | for (auto &c : creatures) { | |
| 349 | + | int flags = 0; | |
| 350 | + | if ((!c.first->isLocalPlayer() || m_drawPlayerBars)) { | |
| 351 | + | if (m_drawHealthBars) { | |
| 352 | + | flags |= Otc::DrawBars; | |
| 353 | + | } | |
| 354 | + | if (m_drawManaBar) { | |
| 355 | + | flags |= Otc::DrawManaBar; | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | c.first->drawInformation(c.second, | |
| 359 | + | g_map.isCovered(c.first->getPrewalkingPosition(), | |
| 360 | + | m_cachedFirstVisibleFloor), | |
| 361 | + | rect, flags); | |
| 362 | + | } | |
| 363 | + | } | |
| 364 | + | ||
| 365 | + | drawTileWidget(rect, srcRect); | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | void MapView::updateVisibleTilesCache() { | |
| 369 | + | int prevFirstVisibleFloor = m_cachedFirstVisibleFloor; | |
| 370 | + | m_cachedFirstVisibleFloor = calcFirstVisibleFloor(false); | |
| 371 | + | m_cachedFirstFadingFloor = calcFirstVisibleFloor(true); | |
| 372 | + | m_cachedLastVisibleFloor = calcLastVisibleFloor(); | |
| 373 | + | ||
| 374 | + | VALIDATE(m_cachedFirstVisibleFloor >= 0 && m_cachedLastVisibleFloor >= 0 && | |
| 375 | + | m_cachedFirstVisibleFloor <= Otc::MAX_Z && | |
| 376 | + | m_cachedLastVisibleFloor <= Otc::MAX_Z); | |
| 377 | + | ||
| 378 | + | if (m_cachedLastVisibleFloor < m_cachedFirstVisibleFloor) | |
| 379 | + | m_cachedLastVisibleFloor = m_cachedFirstVisibleFloor; | |
| 380 | + | ||
| 381 | + | m_mustUpdateVisibleTilesCache = false; | |
| 382 | + | ||
| 383 | + | // there is no tile to render on invalid positions | |
| 384 | + | Position cameraPosition = getCameraPosition(); | |
| 385 | + | if (!cameraPosition.isValid()) { | |
| 386 | + | return; | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | // fading | |
| 390 | + | if (!m_lastCameraPosition.isValid() || | |
| 391 | + | m_lastCameraPosition.z != cameraPosition.z || | |
| 392 | + | m_lastCameraPosition.distance(cameraPosition) >= 3) { | |
| 393 | + | for (int iz = m_cachedLastVisibleFloor; iz >= m_cachedFirstFadingFloor; | |
| 394 | + | --iz) { | |
| 395 | + | m_fadingFloorTimers[iz].restart(m_floorFading * 1000); | |
| 396 | + | } | |
| 397 | + | } else if (prevFirstVisibleFloor < | |
| 398 | + | m_cachedFirstVisibleFloor) { // showing new floor | |
| 399 | + | for (int iz = prevFirstVisibleFloor; iz < m_cachedFirstVisibleFloor; ++iz) { | |
| 400 | + | int shift = std::max<int>( | |
| 401 | + | 0, m_floorFading - m_fadingFloorTimers[iz].elapsed_millis()); | |
| 402 | + | m_fadingFloorTimers[iz].restart(shift * 1000); | |
| 403 | + | } | |
| 404 | + | } else if (prevFirstVisibleFloor > | |
| 405 | + | m_cachedFirstVisibleFloor) { // hiding floor | |
| 406 | + | for (int iz = m_cachedFirstVisibleFloor; iz < prevFirstVisibleFloor; ++iz) { | |
| 407 | + | int shift = std::max<int>( | |
| 408 | + | 0, m_floorFading - m_fadingFloorTimers[iz].elapsed_millis()); | |
| 409 | + | m_fadingFloorTimers[iz].restart(shift * 1000); | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | m_lastCameraPosition = cameraPosition; | |
| 414 | + | ||
| 415 | + | const int numDiagonals = | |
| 416 | + | m_drawDimension.width() + m_drawDimension.height() - 1; | |
| 417 | + | for (auto &cachedVisibleTiles : m_cachedVisibleTiles) { | |
| 418 | + | cachedVisibleTiles.clear(); | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // draw from last floor (the lower) to first floor (the higher) | |
| 422 | + | for (int iz = m_cachedLastVisibleFloor; | |
| 423 | + | iz >= | |
| 424 | + | (m_floorFading ? m_cachedFirstFadingFloor : m_cachedFirstVisibleFloor); | |
| 425 | + | --iz) { | |
| 426 | + | for (int diagonal = 0; diagonal < numDiagonals; ++diagonal) { | |
| 427 | + | // loop current diagonal tiles | |
| 428 | + | int advance = std::max<int>(diagonal - m_drawDimension.height(), 0); | |
| 429 | + | for (int iy = diagonal - advance, ix = advance; | |
| 430 | + | iy >= 0 && ix < m_drawDimension.width(); --iy, ++ix) { | |
| 431 | + | // position on current floor | |
| 432 | + | // TODO: check position limits | |
| 433 | + | Position tilePos = cameraPosition.translated( | |
| 434 | + | ix - m_virtualCenterOffset.x, iy - m_virtualCenterOffset.y); | |
| 435 | + | // adjust tilePos to the wanted floor | |
| 436 | + | tilePos.coveredUp(cameraPosition.z - iz); | |
| 437 | + | if (const TilePtr &tile = g_map.getTile(tilePos)) { | |
| 438 | + | if (!tile->isDrawable()) | |
| 439 | + | continue; | |
| 440 | + | m_cachedVisibleTiles[tilePos.z].push_back(tile); | |
| 441 | + | tile->calculateCorpseCorrection(); | |
| 442 | + | } | |
| 443 | + | } | |
| 444 | + | } | |
| 445 | + | } | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | void MapView::updateGeometry(const Size &visibleDimension, | |
| 449 | + | const Size &optimizedSize) { | |
| 450 | + | m_multifloor = true; | |
| 451 | + | m_visibleDimension = visibleDimension; | |
| 452 | + | m_drawDimension = visibleDimension + Size(3, 3); | |
| 453 | + | m_virtualCenterOffset = (m_drawDimension / 2 - Size(1, 1)).toPoint(); | |
| 454 | + | m_visibleCenterOffset = m_virtualCenterOffset; | |
| 455 | + | m_optimizedSize = m_drawDimension * g_sprites.spriteSize(); | |
| 456 | + | requestVisibleTilesCacheUpdate(); | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | void MapView::onTileUpdate(const Position &pos) { | |
| 460 | + | requestVisibleTilesCacheUpdate(); | |
| 461 | + | } | |
| 462 | + | ||
| 463 | + | void MapView::onMapCenterChange(const Position &pos) { | |
| 464 | + | requestVisibleTilesCacheUpdate(); | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | void MapView::lockFirstVisibleFloor(int firstVisibleFloor) { | |
| 468 | + | m_lockedFirstVisibleFloor = firstVisibleFloor; | |
| 469 | + | requestVisibleTilesCacheUpdate(); | |
| 470 | + | } | |
| 471 | + | ||
| 472 | + | void MapView::unlockFirstVisibleFloor() { | |
| 473 | + | m_lockedFirstVisibleFloor = -1; | |
| 474 | + | requestVisibleTilesCacheUpdate(); | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | void MapView::setVisibleDimension(const Size &visibleDimension) { | |
| 478 | + | // if(visibleDimension == m_visibleDimension) | |
| 479 | + | // return; | |
| 480 | + | ||
| 481 | + | if (visibleDimension.width() % 2 != 1 || visibleDimension.height() % 2 != 1) { | |
| 482 | + | g_logger.traceError("visible dimension must be odd"); | |
| 483 | + | return; | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | if (visibleDimension < Size(3, 3)) { | |
| 487 | + | g_logger.traceError("reach max zoom in"); | |
| 488 | + | return; | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | updateGeometry(visibleDimension, m_optimizedSize); | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | void MapView::optimizeForSize(const Size &visibleSize) { | |
| 495 | + | updateGeometry(m_visibleDimension, visibleSize); | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | void MapView::followCreature(const CreaturePtr &creature) { | |
| 499 | + | m_follow = true; | |
| 500 | + | m_followingCreature = creature; | |
| 501 | + | requestVisibleTilesCacheUpdate(); | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | void MapView::setCameraPosition(const Position &pos) { | |
| 505 | + | m_follow = false; | |
| 506 | + | m_customCameraPosition = pos; | |
| 507 | + | requestVisibleTilesCacheUpdate(); | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | Position MapView::getPosition(const Point &point, const Size &mapSize) { | |
| 511 | + | Position cameraPosition = getCameraPosition(); | |
| 512 | + | ||
| 513 | + | // if we have no camera, its impossible to get the tile | |
| 514 | + | if (!cameraPosition.isValid()) | |
| 515 | + | return Position(); | |
| 516 | + | ||
| 517 | + | Rect srcRect = calcFramebufferSource(mapSize); | |
| 518 | + | float sh = srcRect.width() / (float)mapSize.width(); | |
| 519 | + | float sv = srcRect.height() / (float)mapSize.height(); | |
| 520 | + | ||
| 521 | + | Point framebufferPos = Point(point.x * sh, point.y * sv); | |
| 522 | + | Point realPos = (framebufferPos + srcRect.topLeft()); | |
| 523 | + | Point centerOffset = realPos / g_sprites.spriteSize(); | |
| 524 | + | ||
| 525 | + | Point tilePos2D = getVisibleCenterOffset() - m_drawDimension.toPoint() + | |
| 526 | + | centerOffset + Point(2, 2); | |
| 527 | + | if (tilePos2D.x + cameraPosition.x < 0 && tilePos2D.y + cameraPosition.y < 0) | |
| 528 | + | return Position(); | |
| 529 | + | ||
| 530 | + | Position position = Position(tilePos2D.x, tilePos2D.y, 0) + cameraPosition; | |
| 531 | + | ||
| 532 | + | if (!position.isValid()) | |
| 533 | + | return Position(); | |
| 534 | + | ||
| 535 | + | return position; | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | Point MapView::getPositionOffset(const Point &point, const Size &mapSize) { | |
| 539 | + | Position cameraPosition = getCameraPosition(); | |
| 540 | + | ||
| 541 | + | // if we have no camera, its impossible to get the tile | |
| 542 | + | if (!cameraPosition.isValid()) | |
| 543 | + | return Point(0, 0); | |
| 544 | + | ||
| 545 | + | Rect srcRect = calcFramebufferSource(mapSize); | |
| 546 | + | float sh = srcRect.width() / (float)mapSize.width(); | |
| 547 | + | float sv = srcRect.height() / (float)mapSize.height(); | |
| 548 | + | ||
| 549 | + | Point framebufferPos = Point(point.x * sh, point.y * sv); | |
| 550 | + | Point realPos = (framebufferPos + srcRect.topLeft()); | |
| 551 | + | return Point(realPos.x % g_sprites.spriteSize(), | |
| 552 | + | realPos.y % g_sprites.spriteSize()); | |
| 553 | + | } | |
| 554 | + | ||
| 555 | + | void MapView::move(int x, int y) { | |
| 556 | + | m_moveOffset.x += x; | |
| 557 | + | m_moveOffset.y += y; | |
| 558 | + | ||
| 559 | + | int32_t tmp = m_moveOffset.x / g_sprites.spriteSize(); | |
| 560 | + | bool requestTilesUpdate = false; | |
| 561 | + | if (tmp != 0) { | |
| 562 | + | m_customCameraPosition.x += tmp; | |
| 563 | + | m_moveOffset.x %= g_sprites.spriteSize(); | |
| 564 | + | requestTilesUpdate = true; | |
| 565 | + | } | |
| 566 | + | ||
| 567 | + | tmp = m_moveOffset.y / g_sprites.spriteSize(); | |
| 568 | + | if (tmp != 0) { | |
| 569 | + | m_customCameraPosition.y += tmp; | |
| 570 | + | m_moveOffset.y %= g_sprites.spriteSize(); | |
| 571 | + | requestTilesUpdate = true; | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | if (requestTilesUpdate) | |
| 575 | + | requestVisibleTilesCacheUpdate(); | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | Rect MapView::calcFramebufferSource(const Size &destSize, bool inNextFrame) { | |
| 579 | + | float scaleFactor = g_sprites.spriteSize() / (float)g_sprites.spriteSize(); | |
| 580 | + | Point drawOffset = | |
| 581 | + | ((m_drawDimension - m_visibleDimension - Size(1, 1)).toPoint() / 2) * | |
| 582 | + | g_sprites.spriteSize(); | |
| 583 | + | if (isFollowingCreature()) | |
| 584 | + | drawOffset += m_followingCreature->getWalkOffset(inNextFrame) * scaleFactor; | |
| 585 | + | ||
| 586 | + | Size srcSize = destSize; | |
| 587 | + | Size srcVisible = m_visibleDimension * g_sprites.spriteSize(); | |
| 588 | + | srcSize.scale(srcVisible, Fw::KeepAspectRatio); | |
| 589 | + | drawOffset.x += (srcVisible.width() - srcSize.width()) / 2; | |
| 590 | + | drawOffset.y += (srcVisible.height() - srcSize.height()) / 2; | |
| 591 | + | ||
| 592 | + | return Rect(drawOffset, srcSize); | |
| 593 | + | } | |
| 594 | + | ||
| 595 | + | int MapView::calcFirstVisibleFloor(bool forFading) { | |
| 596 | + | int z = 7; | |
| 597 | + | // return forced first visible floor | |
| 598 | + | if (m_lockedFirstVisibleFloor != -1) { | |
| 599 | + | z = m_lockedFirstVisibleFloor; | |
| 600 | + | } else { | |
| 601 | + | Position cameraPosition = getCameraPosition(); | |
| 602 | + | ||
| 603 | + | // this could happens if the player is not known yet | |
| 604 | + | if (cameraPosition.isValid()) { | |
| 605 | + | // avoid rendering multifloors in far views | |
| 606 | + | if (!m_multifloor) { | |
| 607 | + | z = cameraPosition.z; | |
| 608 | + | } else { | |
| 609 | + | // if nothing is limiting the view, the first visible floor is 0 | |
| 610 | + | int firstFloor = 0; | |
| 611 | + | ||
| 612 | + | // limits to underground floors while under sea level | |
| 613 | + | if (cameraPosition.z > Otc::SEA_FLOOR) | |
| 614 | + | firstFloor = std::max<int>(cameraPosition.z - | |
| 615 | + | Otc::AWARE_UNDEGROUND_FLOOR_RANGE, | |
| 616 | + | (int)Otc::UNDERGROUND_FLOOR); | |
| 617 | + | ||
| 618 | + | // loop in 3x3 tiles around the camera | |
| 619 | + | for (int ix = -1; | |
| 620 | + | ix <= 1 && firstFloor < cameraPosition.z && !forFading; ++ix) { | |
| 621 | + | for (int iy = -1; iy <= 1 && firstFloor < cameraPosition.z; ++iy) { | |
| 622 | + | Position pos = cameraPosition.translated(ix, iy); | |
| 623 | + | ||
| 624 | + | // process tiles that we can look through, e.g. windows, doors | |
| 625 | + | if ((ix == 0 && iy == 0) || | |
| 626 | + | ((std::abs(ix) != std::abs(iy)) && g_map.isLookPossible(pos))) { | |
| 627 | + | Position upperPos = pos; | |
| 628 | + | Position coveredPos = pos; | |
| 629 | + | ||
| 630 | + | while (coveredPos.coveredUp() && upperPos.up() && | |
| 631 | + | upperPos.z >= firstFloor) { | |
| 632 | + | // check tiles physically above | |
| 633 | + | TilePtr tile = g_map.getTile(upperPos); | |
| 634 | + | if (tile && | |
| 635 | + | tile->limitsFloorsView(!g_map.isLookPossible(pos))) { | |
| 636 | + | firstFloor = upperPos.z + 1; | |
| 637 | + | break; | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // check tiles geometrically above | |
| 641 | + | tile = g_map.getTile(coveredPos); | |
| 642 | + | if (tile && tile->limitsFloorsView(g_map.isLookPossible(pos))) { | |
| 643 | + | firstFloor = coveredPos.z + 1; | |
| 644 | + | break; | |
| 645 | + | } | |
| 646 | + | } | |
| 647 | + | } | |
| 648 | + | } | |
| 649 | + | } | |
| 650 | + | z = firstFloor; | |
| 651 | + | } | |
| 652 | + | } | |
| 653 | + | } | |
| 654 | + | ||
| 655 | + | // just ensure the that the floor is in the valid range | |
| 656 | + | z = stdext::clamp<int>(z, 0, (int)Otc::MAX_Z); | |
| 657 | + | return z; | |
| 658 | + | } | |
| 659 | + | ||
| 660 | + | int MapView::calcLastVisibleFloor() { | |
| 661 | + | if (!m_multifloor) | |
| 662 | + | return calcFirstVisibleFloor(); | |
| 663 | + | ||
| 664 | + | int z = 7; | |
| 665 | + | ||
| 666 | + | Position cameraPosition = getCameraPosition(); | |
| 667 | + | // this could happens if the player is not known yet | |
| 668 | + | if (cameraPosition.isValid()) { | |
| 669 | + | // view only underground floors when below sea level | |
| 670 | + | if (cameraPosition.z > Otc::SEA_FLOOR) | |
| 671 | + | z = cameraPosition.z + Otc::AWARE_UNDEGROUND_FLOOR_RANGE; | |
| 672 | + | else | |
| 673 | + | // FIX (OTLand/OTCv8): Server sends floors 15->0 for ALL surface players. | |
| 674 | + | // Render down to floor 15 so underground tiles are visible through | |
| 675 | + | // empty/transparent holes in floor 7 (cave entrances, water, etc.) | |
| 676 | + | z = (int)Otc::MAX_Z; // Render all floors when at surface | |
| 677 | + | } | |
| 678 | + | ||
| 679 | + | if (m_lockedFirstVisibleFloor != -1) | |
| 680 | + | z = std::max<int>(m_lockedFirstVisibleFloor, z); | |
| 681 | + | ||
| 682 | + | // just ensure the that the floor is in the valid range | |
| 683 | + | z = stdext::clamp<int>(z, 0, (int)Otc::MAX_Z); | |
| 684 | + | return z; | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | Point MapView::transformPositionTo2D(const Position &position, | |
| 688 | + | const Position &relativePosition) { | |
| 689 | + | return Point((m_virtualCenterOffset.x + (position.x - relativePosition.x) - | |
| 690 | + | (relativePosition.z - position.z)) * | |
| 691 | + | g_sprites.spriteSize(), | |
| 692 | + | (m_virtualCenterOffset.y + (position.y - relativePosition.y) - | |
| 693 | + | (relativePosition.z - position.z)) * | |
| 694 | + | g_sprites.spriteSize()); | |
| 695 | + | } | |
| 696 | + | ||
| 697 | + | Position MapView::getCameraPosition() { | |
| 698 | + | if (isFollowingCreature()) { | |
| 699 | + | return m_followingCreature->getPrewalkingPosition(); | |
| 700 | + | } | |
| 701 | + | ||
| 702 | + | return m_customCameraPosition; | |
| 703 | + | } | |
| 704 | + | ||
| 705 | + | void MapView::setDrawLights(bool enable) { m_drawLight = enable; } | |
| 706 | + | ||
| 707 | + | void MapView::setCrosshair(const std::string &file) { | |
| 708 | + | if (file == "") | |
| 709 | + | m_crosshair = nullptr; | |
| 710 | + | else | |
| 711 | + | m_crosshair = g_textures.getTexture(file); | |
| 712 | + | } | |
| 713 | + | ||
| 714 | + | /* vim: set ts=4 sw=4 et: */ |
| @@ -0,0 +1,531 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "minimap.h" | |
| 24 | + | #include "game.h" | |
| 25 | + | #include "spritemanager.h" | |
| 26 | + | #include "tile.h" | |
| 27 | + | ||
| 28 | + | #include <framework/core/filestream.h> | |
| 29 | + | #include <framework/core/resourcemanager.h> | |
| 30 | + | #include <framework/graphics/framebuffermanager.h> | |
| 31 | + | #include <framework/graphics/image.h> | |
| 32 | + | #include <framework/graphics/painter.h> | |
| 33 | + | #include <framework/graphics/texture.h> | |
| 34 | + | #include <zlib.h> | |
| 35 | + | ||
| 36 | + | #include <framework/util/stats.h> | |
| 37 | + | ||
| 38 | + | Minimap g_minimap; | |
| 39 | + | ||
| 40 | + | void MinimapBlock::clean() { | |
| 41 | + | m_tiles.fill(MinimapTile()); | |
| 42 | + | m_texture.reset(); | |
| 43 | + | m_mustUpdate = false; | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | void MinimapBlock::update() { | |
| 47 | + | if (!m_mustUpdate) | |
| 48 | + | return; | |
| 49 | + | ||
| 50 | + | ImagePtr image(new Image(Size(MMBLOCK_SIZE, MMBLOCK_SIZE))); | |
| 51 | + | ||
| 52 | + | bool shouldDraw = false; | |
| 53 | + | for (int x = 0; x < MMBLOCK_SIZE; ++x) { | |
| 54 | + | for (int y = 0; y < MMBLOCK_SIZE; ++y) { | |
| 55 | + | uint8 c = getTile(x, y).color; | |
| 56 | + | Color col = Color::alpha; | |
| 57 | + | if (c != 255) { | |
| 58 | + | col = Color::from8bit(c); | |
| 59 | + | shouldDraw = true; | |
| 60 | + | } | |
| 61 | + | image->setPixel(x, y, col); | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | if (shouldDraw) { | |
| 66 | + | m_texture = TexturePtr(new Texture(image)); | |
| 67 | + | } else | |
| 68 | + | m_texture.reset(); | |
| 69 | + | ||
| 70 | + | m_mustUpdate = false; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | void MinimapBlock::updateTile(int x, int y, const MinimapTile &tile) { | |
| 74 | + | if (m_tiles[getTileIndex(x, y)].color != tile.color) | |
| 75 | + | m_mustUpdate = true; | |
| 76 | + | ||
| 77 | + | m_tiles[getTileIndex(x, y)] = tile; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | void Minimap::init() {} | |
| 81 | + | ||
| 82 | + | void Minimap::terminate() { clean(); } | |
| 83 | + | ||
| 84 | + | void Minimap::clean() { | |
| 85 | + | std::lock_guard<std::mutex> lock(m_lock); | |
| 86 | + | for (int i = 0; i <= Otc::MAX_Z; ++i) | |
| 87 | + | m_tileBlocks[i].clear(); | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | void Minimap::draw(const Rect &screenRect, const Position &mapCenter, | |
| 91 | + | float scale, const Color &color) { | |
| 92 | + | if (screenRect.isEmpty()) | |
| 93 | + | return; | |
| 94 | + | ||
| 95 | + | Rect mapRect = | |
| 96 | + | calcMapRect(screenRect, mapCenter, | |
| 97 | + | scale); // Draw background (disabled to avoid square shape) | |
| 98 | + | // g_drawQueue->addFilledRect(screenRect, color); | |
| 99 | + | ||
| 100 | + | if (MMBLOCK_SIZE * scale <= 1 || !mapCenter.isMapPosition()) { | |
| 101 | + | return; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | size_t drawQueueStart = g_drawQueue->size(); | |
| 105 | + | Point blockOff = getBlockOffset(mapRect.topLeft()); | |
| 106 | + | Point off = | |
| 107 | + | Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) / | |
| 108 | + | 2; | |
| 109 | + | Point start = | |
| 110 | + | screenRect.topLeft() - (mapRect.topLeft() - blockOff) * scale - off; | |
| 111 | + | ||
| 112 | + | // Circle clip: compute center and squared radius from screenRect | |
| 113 | + | int circleX = screenRect.left() + screenRect.width() / 2; | |
| 114 | + | int circleY = screenRect.top() + screenRect.height() / 2; | |
| 115 | + | int circleR = std::min(screenRect.width(), screenRect.height()) / 2; | |
| 116 | + | int circleR2 = circleR * circleR; | |
| 117 | + | ||
| 118 | + | for (int y = blockOff.y, ys = start.y; ys < screenRect.bottom(); | |
| 119 | + | y += MMBLOCK_SIZE, ys += MMBLOCK_SIZE * scale) { | |
| 120 | + | if (y < 0 || y >= 65536) | |
| 121 | + | continue; | |
| 122 | + | ||
| 123 | + | for (int x = blockOff.x, xs = start.x; xs < screenRect.right(); | |
| 124 | + | x += MMBLOCK_SIZE, xs += MMBLOCK_SIZE * scale) { | |
| 125 | + | if (x < 0 || x >= 65536) | |
| 126 | + | continue; | |
| 127 | + | ||
| 128 | + | Position blockPos(x, y, mapCenter.z); | |
| 129 | + | if (!hasBlock(blockPos)) | |
| 130 | + | continue; | |
| 131 | + | ||
| 132 | + | MinimapBlock &block = getBlock(Position(x, y, mapCenter.z)); | |
| 133 | + | block.update(); | |
| 134 | + | ||
| 135 | + | const TexturePtr &tex = block.getTexture(); | |
| 136 | + | if (tex) { | |
| 137 | + | Rect src(0, 0, MMBLOCK_SIZE, MMBLOCK_SIZE); | |
| 138 | + | float fscale = (float)scale; | |
| 139 | + | Rect dest(xs, ys, (int)(MMBLOCK_SIZE * fscale), | |
| 140 | + | (int)(MMBLOCK_SIZE * fscale)); | |
| 141 | + | ||
| 142 | + | // Circle check: skip tiles whose nearest point to circle center | |
| 143 | + | // is outside the circle radius (corners won't render outside ring). | |
| 144 | + | { | |
| 145 | + | int nearX = std::max(dest.left(), std::min(circleX, dest.right())); | |
| 146 | + | int nearY = std::max(dest.top(), std::min(circleY, dest.bottom())); | |
| 147 | + | int dx = nearX - circleX; | |
| 148 | + | int dy = nearY - circleY; | |
| 149 | + | if (dx * dx + dy * dy > circleR2) | |
| 150 | + | continue; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // Sub-block drawing for smoother circular edges | |
| 154 | + | int subDiv = 64; | |
| 155 | + | int subSize = MMBLOCK_SIZE / subDiv; | |
| 156 | + | float subDestSizeX = (float)dest.width() / (float)subDiv; | |
| 157 | + | float subDestSizeY = (float)dest.height() / (float)subDiv; | |
| 158 | + | float subSrcSizeX = (float)src.width() / (float)subDiv; | |
| 159 | + | float subSrcSizeY = (float)src.height() / (float)subDiv; | |
| 160 | + | ||
| 161 | + | for (int sy = 0; sy < subDiv; ++sy) { | |
| 162 | + | int yStart = (int)(sy * subDestSizeY); | |
| 163 | + | int yEnd = (int)((sy + 1) * subDestSizeY); | |
| 164 | + | int syStart = (int)(sy * subSrcSizeY); | |
| 165 | + | int syEnd = (int)((sy + 1) * subSrcSizeY); | |
| 166 | + | ||
| 167 | + | for (int sx = 0; sx < subDiv; ++sx) { | |
| 168 | + | int xStart = (int)(sx * subDestSizeX); | |
| 169 | + | int xEnd = (int)((sx + 1) * subDestSizeX); | |
| 170 | + | ||
| 171 | + | Rect subDest(dest.left() + xStart, dest.top() + yStart, | |
| 172 | + | xEnd - xStart, yEnd - yStart); | |
| 173 | + | ||
| 174 | + | // Check if all four corners of subDest are within the circle | |
| 175 | + | bool inside = true; | |
| 176 | + | Point corners[4] = {subDest.topLeft(), subDest.topRight(), | |
| 177 | + | subDest.bottomLeft(), subDest.bottomRight()}; | |
| 178 | + | for (const Point &p : corners) { | |
| 179 | + | int dx = p.x - circleX; | |
| 180 | + | int dy = p.y - circleY; | |
| 181 | + | if (dx * dx + dy * dy > circleR2) { | |
| 182 | + | inside = false; | |
| 183 | + | break; | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | if (!inside) | |
| 187 | + | continue; | |
| 188 | + | ||
| 189 | + | int sxStart = (int)(sx * subSrcSizeX); | |
| 190 | + | int sxEnd = (int)((sx + 1) * subSrcSizeX); | |
| 191 | + | ||
| 192 | + | Rect subSrc(src.left() + sxStart, src.top() + syStart, | |
| 193 | + | sxEnd - sxStart, syEnd - syStart); | |
| 194 | + | ||
| 195 | + | // Rect clip: clip subDest to screenRect | |
| 196 | + | if (!screenRect.contains(subDest)) { | |
| 197 | + | if (!subDest.intersects(screenRect)) | |
| 198 | + | continue; | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | g_drawQueue->addTexturedRect(subDest, tex, subSrc); | |
| 202 | + | } | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | g_drawQueue->setClip(drawQueueStart, screenRect); | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | Point Minimap::getTilePoint(const Position &pos, const Rect &screenRect, | |
| 212 | + | const Position &mapCenter, float scale) { | |
| 213 | + | if (screenRect.isEmpty() || pos.z != mapCenter.z) | |
| 214 | + | return Point(-1, -1); | |
| 215 | + | ||
| 216 | + | Rect mapRect = calcMapRect(screenRect, mapCenter, scale); | |
| 217 | + | Point off = | |
| 218 | + | Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) / | |
| 219 | + | 2; | |
| 220 | + | Point posoff = (Point(pos.x, pos.y) - mapRect.topLeft()) * scale; | |
| 221 | + | return posoff + screenRect.topLeft() - off + (Point(1, 1) * scale) / 2; | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | Position Minimap::getTilePosition(const Point &point, const Rect &screenRect, | |
| 225 | + | const Position &mapCenter, float scale) { | |
| 226 | + | if (screenRect.isEmpty()) | |
| 227 | + | return Position(); | |
| 228 | + | ||
| 229 | + | Rect mapRect = calcMapRect(screenRect, mapCenter, scale); | |
| 230 | + | Point off = | |
| 231 | + | Point((mapRect.size() * scale).toPoint() - screenRect.size().toPoint()) / | |
| 232 | + | 2; | |
| 233 | + | Point pos2d = | |
| 234 | + | (point - screenRect.topLeft() + off) / scale + mapRect.topLeft(); | |
| 235 | + | return Position(pos2d.x, pos2d.y, mapCenter.z); | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | Rect Minimap::getTileRect(const Position &pos, const Rect &screenRect, | |
| 239 | + | const Position &mapCenter, float scale) { | |
| 240 | + | if (screenRect.isEmpty() || pos.z != mapCenter.z) | |
| 241 | + | return Rect(); | |
| 242 | + | ||
| 243 | + | int tileSize = g_sprites.spriteSize() * scale; | |
| 244 | + | Rect tileRect(0, 0, tileSize, tileSize); | |
| 245 | + | tileRect.moveCenter(getTilePoint(pos, screenRect, mapCenter, scale)); | |
| 246 | + | return tileRect; | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | Rect Minimap::calcMapRect(const Rect &screenRect, const Position &mapCenter, | |
| 250 | + | float scale) { | |
| 251 | + | int w = screenRect.width() / scale, | |
| 252 | + | h = std::ceil(screenRect.height() / scale); | |
| 253 | + | Rect mapRect(0, 0, w, h); | |
| 254 | + | mapRect.moveCenter(Point(mapCenter.x, mapCenter.y)); | |
| 255 | + | return mapRect; | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | void Minimap::updateTile(const Position &pos, const TilePtr &tile) { | |
| 259 | + | MinimapTile minimapTile; | |
| 260 | + | if (tile) { | |
| 261 | + | minimapTile.color = tile->getMinimapColorByte(); | |
| 262 | + | minimapTile.flags |= MinimapTileWasSeen; | |
| 263 | + | if (!tile->isWalkable(true)) | |
| 264 | + | minimapTile.flags |= MinimapTileNotWalkable; | |
| 265 | + | if (!tile->isPathable()) | |
| 266 | + | minimapTile.flags |= MinimapTileNotPathable; | |
| 267 | + | minimapTile.speed = | |
| 268 | + | std::min<int>((int)std::ceil(tile->getGroundSpeed() / 10.0f), 255); | |
| 269 | + | } else { | |
| 270 | + | minimapTile.color = 255; | |
| 271 | + | minimapTile.flags |= MinimapTileEmpty; | |
| 272 | + | minimapTile.speed = 1; | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | if (minimapTile != MinimapTile()) { | |
| 276 | + | MinimapBlock &block = getBlock(pos); | |
| 277 | + | Point offsetPos = getBlockOffset(Point(pos.x, pos.y)); | |
| 278 | + | block.updateTile(pos.x - offsetPos.x, pos.y - offsetPos.y, minimapTile); | |
| 279 | + | block.justSaw(); | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | const MinimapTile &Minimap::getTile(const Position &pos) { | |
| 284 | + | static MinimapTile nulltile; | |
| 285 | + | if (pos.z <= Otc::MAX_Z && hasBlock(pos)) { | |
| 286 | + | MinimapBlock &block = getBlock(pos); | |
| 287 | + | Point offsetPos = getBlockOffset(Point(pos.x, pos.y)); | |
| 288 | + | return block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y); | |
| 289 | + | } | |
| 290 | + | return nulltile; | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | std::pair<MinimapBlock_ptr, MinimapTile> | |
| 294 | + | Minimap::threadGetTile(const Position &pos) { | |
| 295 | + | std::lock_guard<std::mutex> lock(m_lock); | |
| 296 | + | static MinimapTile nulltile; | |
| 297 | + | ||
| 298 | + | if (pos.z <= Otc::MAX_Z && hasBlock(pos)) { | |
| 299 | + | MinimapBlock_ptr block = m_tileBlocks[pos.z][getBlockIndex(pos)]; | |
| 300 | + | if (block) { | |
| 301 | + | Point offsetPos = getBlockOffset(Point(pos.x, pos.y)); | |
| 302 | + | return std::make_pair( | |
| 303 | + | block, block->getTile(pos.x - offsetPos.x, pos.y - offsetPos.y)); | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | return std::make_pair(nullptr, nulltile); | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | bool Minimap::loadImage(const std::string &fileName, const Position &topLeft, | |
| 310 | + | float colorFactor) { | |
| 311 | + | if (colorFactor <= 0.01f) | |
| 312 | + | colorFactor = 1.0f; | |
| 313 | + | ||
| 314 | + | try { | |
| 315 | + | ImagePtr image = Image::load(fileName); | |
| 316 | + | ||
| 317 | + | uint8 waterc = Color::to8bit(std::string("#3300cc")); | |
| 318 | + | ||
| 319 | + | // non pathable colors | |
| 320 | + | Color nonPathableColors[] = { | |
| 321 | + | std::string("#ffff00"), // yellow | |
| 322 | + | }; | |
| 323 | + | ||
| 324 | + | // non walkable colors | |
| 325 | + | Color nonWalkableColors[] = { | |
| 326 | + | std::string("#000000"), // oil, black | |
| 327 | + | std::string("#006600"), // trees, dark green | |
| 328 | + | std::string("#ff3300"), // walls, red | |
| 329 | + | std::string("#666666"), // mountain, grey | |
| 330 | + | std::string("#ff6600"), // lava, orange | |
| 331 | + | std::string("#00ff00"), // positon | |
| 332 | + | std::string("#ccffff"), // ice, very light blue | |
| 333 | + | }; | |
| 334 | + | ||
| 335 | + | for (int y = 0; y < image->getHeight(); ++y) { | |
| 336 | + | for (int x = 0; x < image->getWidth(); ++x) { | |
| 337 | + | Color color = *(uint32 *)image->getPixel(x, y); | |
| 338 | + | uint8 c = Color::to8bit(color * colorFactor); | |
| 339 | + | int flags = 0; | |
| 340 | + | ||
| 341 | + | if (c == waterc || color.a() == 0) { | |
| 342 | + | flags |= MinimapTileNotWalkable; | |
| 343 | + | c = 255; // alpha | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | if (flags != 0) { | |
| 347 | + | for (Color &col : nonWalkableColors) { | |
| 348 | + | if (col == color) { | |
| 349 | + | flags |= MinimapTileNotWalkable; | |
| 350 | + | break; | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | if (flags != 0) { | |
| 356 | + | for (Color &col : nonPathableColors) { | |
| 357 | + | if (col == color) { | |
| 358 | + | flags |= MinimapTileNotPathable; | |
| 359 | + | break; | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | if (c == 255) | |
| 365 | + | continue; | |
| 366 | + | ||
| 367 | + | Position pos(topLeft.x + x, topLeft.y + y, topLeft.z); | |
| 368 | + | MinimapBlock &block = getBlock(pos); | |
| 369 | + | Point offsetPos = getBlockOffset(Point(pos.x, pos.y)); | |
| 370 | + | MinimapTile &tile = | |
| 371 | + | block.getTile(pos.x - offsetPos.x, pos.y - offsetPos.y); | |
| 372 | + | if (!(tile.flags & MinimapTileWasSeen)) { | |
| 373 | + | tile.color = c; | |
| 374 | + | tile.flags = flags; | |
| 375 | + | block.mustUpdate(); | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | } | |
| 379 | + | return true; | |
| 380 | + | } catch (stdext::exception &e) { | |
| 381 | + | g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what())); | |
| 382 | + | return false; | |
| 383 | + | } | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | void Minimap::saveImage(const std::string &fileName, const Rect &mapRect) { | |
| 387 | + | // TODO | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | bool Minimap::loadOtmm(const std::string &fileName) { | |
| 391 | + | try { | |
| 392 | + | FileStreamPtr fin = g_resources.openFile( | |
| 393 | + | fileName, g_game.getFeature(Otc::GameDontCacheFiles)); | |
| 394 | + | if (!fin) | |
| 395 | + | stdext::throw_exception("unable to open file"); | |
| 396 | + | ||
| 397 | + | uint32 signature = fin->getU32(); | |
| 398 | + | if (signature != OTMM_SIGNATURE) | |
| 399 | + | stdext::throw_exception("invalid OTMM file"); | |
| 400 | + | ||
| 401 | + | uint16 start = fin->getU16(); | |
| 402 | + | uint16 version = fin->getU16(); | |
| 403 | + | fin->getU32(); // flags | |
| 404 | + | ||
| 405 | + | switch (version) { | |
| 406 | + | case 1: { | |
| 407 | + | fin->getString(); // description | |
| 408 | + | break; | |
| 409 | + | } | |
| 410 | + | default: | |
| 411 | + | stdext::throw_exception("OTMM version not supported"); | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | fin->seek(start); | |
| 415 | + | ||
| 416 | + | uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile); | |
| 417 | + | std::vector<uchar> compressBuffer(compressBound(blockSize)); | |
| 418 | + | std::vector<uchar> decompressBuffer(blockSize); | |
| 419 | + | ||
| 420 | + | while (true) { | |
| 421 | + | Position pos; | |
| 422 | + | pos.x = fin->getU16(); | |
| 423 | + | pos.y = fin->getU16(); | |
| 424 | + | pos.z = fin->getU8(); | |
| 425 | + | ||
| 426 | + | // end of file or file is corrupted | |
| 427 | + | if (!pos.isValid() || pos.z >= Otc::MAX_Z + 1) | |
| 428 | + | break; | |
| 429 | + | ||
| 430 | + | MinimapBlock &block = getBlock(pos); | |
| 431 | + | ulong len = fin->getU16(); | |
| 432 | + | ulong destLen = blockSize; | |
| 433 | + | fin->read(compressBuffer.data(), len); | |
| 434 | + | int ret = uncompress(decompressBuffer.data(), &destLen, | |
| 435 | + | compressBuffer.data(), len); | |
| 436 | + | if (ret != Z_OK || destLen != blockSize) | |
| 437 | + | break; | |
| 438 | + | ||
| 439 | + | memcpy((uchar *)&block.getTiles(), decompressBuffer.data(), blockSize); | |
| 440 | + | block.mustUpdate(); | |
| 441 | + | block.justSaw(); | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | fin->close(); | |
| 445 | + | return true; | |
| 446 | + | } catch (stdext::exception &e) { | |
| 447 | + | g_logger.error(stdext::format("failed to load OTMM minimap: %s", e.what())); | |
| 448 | + | return false; | |
| 449 | + | } | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | void Minimap::saveOtmm(const std::string &fileName) { | |
| 453 | + | try { | |
| 454 | + | stdext::timer saveTimer; | |
| 455 | + | ||
| 456 | + | #ifndef ANDROID | |
| 457 | + | std::string tmpFileName = fileName; | |
| 458 | + | tmpFileName += ".tmp"; | |
| 459 | + | FileStreamPtr fin = g_resources.createFile(tmpFileName); | |
| 460 | + | #else | |
| 461 | + | FileStreamPtr fin = g_resources.createFile(fileName); | |
| 462 | + | #endif | |
| 463 | + | ||
| 464 | + | // TODO: compression flag with zlib | |
| 465 | + | uint32 flags = 0; | |
| 466 | + | ||
| 467 | + | // header | |
| 468 | + | fin->addU32(OTMM_SIGNATURE); | |
| 469 | + | fin->addU16(0); // data start, will be overwritten later | |
| 470 | + | fin->addU16(OTMM_VERSION); | |
| 471 | + | fin->addU32(flags); | |
| 472 | + | ||
| 473 | + | // version 1 header | |
| 474 | + | fin->addString("OTMM 1.0"); // description | |
| 475 | + | ||
| 476 | + | // go back and rewrite where the map data starts | |
| 477 | + | uint32 start = fin->tell(); | |
| 478 | + | fin->seek(4); | |
| 479 | + | fin->addU16(start); | |
| 480 | + | fin->seek(start); | |
| 481 | + | ||
| 482 | + | uint blockSize = MMBLOCK_SIZE * MMBLOCK_SIZE * sizeof(MinimapTile); | |
| 483 | + | std::vector<uchar> compressBuffer(compressBound(blockSize)); | |
| 484 | + | const int COMPRESS_LEVEL = 3; | |
| 485 | + | ||
| 486 | + | for (uint8_t z = 0; z <= Otc::MAX_Z; ++z) { | |
| 487 | + | for (auto &it : m_tileBlocks[z]) { | |
| 488 | + | int index = it.first; | |
| 489 | + | MinimapBlock &block = *it.second; | |
| 490 | + | if (!block.wasSeen()) | |
| 491 | + | continue; | |
| 492 | + | ||
| 493 | + | Position pos = getIndexPosition(index, z); | |
| 494 | + | fin->addU16(pos.x); | |
| 495 | + | fin->addU16(pos.y); | |
| 496 | + | fin->addU8(pos.z); | |
| 497 | + | ||
| 498 | + | ulong len = blockSize; | |
| 499 | + | int ret = | |
| 500 | + | compress2(compressBuffer.data(), &len, (uchar *)&block.getTiles(), | |
| 501 | + | blockSize, COMPRESS_LEVEL); | |
| 502 | + | VALIDATE(ret == Z_OK); | |
| 503 | + | fin->addU16(len); | |
| 504 | + | fin->write(compressBuffer.data(), len); | |
| 505 | + | } | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // end of file | |
| 509 | + | Position invalidPos; | |
| 510 | + | fin->addU16(invalidPos.x); | |
| 511 | + | fin->addU16(invalidPos.y); | |
| 512 | + | fin->addU8(invalidPos.z); | |
| 513 | + | ||
| 514 | + | fin->flush(); | |
| 515 | + | ||
| 516 | + | fin->close(); | |
| 517 | + | #ifndef ANDROID | |
| 518 | + | std::filesystem::path filePath(g_resources.getWriteDir()), | |
| 519 | + | tmpFilePath(g_resources.getWriteDir()); | |
| 520 | + | filePath += fileName; | |
| 521 | + | tmpFilePath += tmpFileName; | |
| 522 | + | if (std::filesystem::file_size(tmpFilePath) > 1024) { | |
| 523 | + | std::filesystem::rename(tmpFilePath, filePath); | |
| 524 | + | } | |
| 525 | + | #endif | |
| 526 | + | } catch (stdext::exception &e) { | |
| 527 | + | g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what())); | |
| 528 | + | } catch (std::exception &e) { | |
| 529 | + | g_logger.error(stdext::format("failed to save OTMM minimap: %s", e.what())); | |
| 530 | + | } | |
| 531 | + | } |
| @@ -0,0 +1,106 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "animatedtext.h" | |
| 24 | + | #include "map.h" | |
| 25 | + | #include "game.h" | |
| 26 | + | #include <framework/core/clock.h> | |
| 27 | + | #include <framework/core/eventdispatcher.h> | |
| 28 | + | #include <framework/graphics/graphics.h> | |
| 29 | + | ||
| 30 | + | AnimatedText::AnimatedText() | |
| 31 | + | { | |
| 32 | + | m_cachedText.setFont(g_fonts.getFont("verdana-11px-rounded")); | |
| 33 | + | m_cachedText.setAlign(Fw::AlignLeft); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | void AnimatedText::drawText(const Point& dest, const Rect& visibleRect) | |
| 37 | + | { | |
| 38 | + | static float tf = Otc::ANIMATED_TEXT_DURATION; | |
| 39 | + | static float tftf = Otc::ANIMATED_TEXT_DURATION * Otc::ANIMATED_TEXT_DURATION; | |
| 40 | + | ||
| 41 | + | Point p = dest; | |
| 42 | + | Size textSize = m_cachedText.getTextSize(); | |
| 43 | + | float t = m_animationTimer.ticksElapsed(); | |
| 44 | + | p.x -= textSize.width() / 2; | |
| 45 | + | ||
| 46 | + | if(g_game.getFeature(Otc::GameDiagonalAnimatedText)) { | |
| 47 | + | p.x -= (4 * t / tf) + (8 * t * t / tftf); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | p.y += (-48 * t) / tf; | |
| 51 | + | p += m_offset; | |
| 52 | + | Rect rect(p, textSize); | |
| 53 | + | ||
| 54 | + | if(visibleRect.contains(rect)) { | |
| 55 | + | float t0 = tf / 1.2; | |
| 56 | + | Color color = m_color; | |
| 57 | + | if(t > t0) { | |
| 58 | + | color.setAlpha((float)(1 - (t - t0) / (tf - t0))); | |
| 59 | + | } | |
| 60 | + | m_cachedText.draw(rect, color); | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | void AnimatedText::onAppear() | |
| 65 | + | { | |
| 66 | + | m_animationTimer.restart(); | |
| 67 | + | ||
| 68 | + | // schedule removal | |
| 69 | + | auto self = asAnimatedText(); | |
| 70 | + | g_dispatcher.scheduleEvent([self]() { g_map.removeThing(self); }, Otc::ANIMATED_TEXT_DURATION); | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | void AnimatedText::setColor(int color) | |
| 74 | + | { | |
| 75 | + | m_color = Color::from8bit(color); | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | void AnimatedText::setText(const std::string& text) | |
| 79 | + | { | |
| 80 | + | m_cachedText.setText(text); | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | void AnimatedText::setFont(const std::string& fontName) | |
| 84 | + | { | |
| 85 | + | m_cachedText.setFont(g_fonts.getFont(fontName)); | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | bool AnimatedText::merge(const AnimatedTextPtr& other) | |
| 89 | + | { | |
| 90 | + | if(other->getColor() != m_color) | |
| 91 | + | return false; | |
| 92 | + | ||
| 93 | + | if(other->getCachedText().getFont() != m_cachedText.getFont()) | |
| 94 | + | return false; | |
| 95 | + | ||
| 96 | + | if(m_animationTimer.ticksElapsed() > (int)Otc::ANIMATED_TEXT_DURATION / 2.5) | |
| 97 | + | return false; | |
| 98 | + | ||
| 99 | + | try { | |
| 100 | + | int number = stdext::safe_cast<int>(m_cachedText.getText()); | |
| 101 | + | int otherNumber = stdext::safe_cast<int>(other->getCachedText().getText()); | |
| 102 | + | m_cachedText.setText(std::to_string(number + otherNumber)); | |
| 103 | + | return true; | |
| 104 | + | } catch(...) {} | |
| 105 | + | return false; | |
| 106 | + | } |
| @@ -0,0 +1,65 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #ifndef ANIMATEDTEXT_H | |
| 24 | + | #define ANIMATEDTEXT_H | |
| 25 | + | ||
| 26 | + | #include "thing.h" | |
| 27 | + | #include <framework/graphics/fontmanager.h> | |
| 28 | + | #include <framework/core/timer.h> | |
| 29 | + | #include <framework/graphics/cachedtext.h> | |
| 30 | + | ||
| 31 | + | // @bindclass | |
| 32 | + | class AnimatedText : public Thing | |
| 33 | + | { | |
| 34 | + | public: | |
| 35 | + | AnimatedText(); | |
| 36 | + | ||
| 37 | + | void drawText(const Point& dest, const Rect& visibleRect); | |
| 38 | + | ||
| 39 | + | void setColor(int color); | |
| 40 | + | void setText(const std::string& text); | |
| 41 | + | void setOffset(const Point& offset) { m_offset = offset; } | |
| 42 | + | void setFont(const std::string& fontName); | |
| 43 | + | ||
| 44 | + | Color getColor() { return m_color; } | |
| 45 | + | const CachedText& getCachedText() const { return m_cachedText; } | |
| 46 | + | Point getOffset() { return m_offset; } | |
| 47 | + | Timer getTimer() { return m_animationTimer; } | |
| 48 | + | ||
| 49 | + | bool merge(const AnimatedTextPtr& other); | |
| 50 | + | ||
| 51 | + | AnimatedTextPtr asAnimatedText() { return static_self_cast<AnimatedText>(); } | |
| 52 | + | bool isAnimatedText() { return true; } | |
| 53 | + | std::string getText() { return m_cachedText.getText(); } | |
| 54 | + | ||
| 55 | + | protected: | |
| 56 | + | virtual void onAppear(); | |
| 57 | + | ||
| 58 | + | private: | |
| 59 | + | Color m_color; | |
| 60 | + | Timer m_animationTimer; | |
| 61 | + | CachedText m_cachedText; | |
| 62 | + | Point m_offset; | |
| 63 | + | }; | |
| 64 | + | ||
| 65 | + | #endif |
| @@ -0,0 +1,240 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "animator.h" | |
| 24 | + | #include "declarations.h" | |
| 25 | + | ||
| 26 | + | #include <framework/core/clock.h> | |
| 27 | + | #include <framework/core/filestream.h> | |
| 28 | + | #include <framework/stdext/fastrand.h> | |
| 29 | + | #include <framework/util/extras.h> | |
| 30 | + | ||
| 31 | + | Animator::Animator() { | |
| 32 | + | m_animationPhases = 0; | |
| 33 | + | m_startPhase = 0; | |
| 34 | + | m_loopCount = 0; | |
| 35 | + | m_async = false; | |
| 36 | + | m_currentDuration = 0; | |
| 37 | + | m_currentDirection = AnimDirForward; | |
| 38 | + | m_currentLoop = 0; | |
| 39 | + | m_lastPhaseTicks = 0; | |
| 40 | + | m_isComplete = false; | |
| 41 | + | m_phase = 0; | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | void Animator::unserialize(int animationPhases, const FileStreamPtr &fin) { | |
| 45 | + | m_animationPhases = animationPhases; | |
| 46 | + | m_async = fin->getU8() == 0; | |
| 47 | + | m_loopCount = fin->get32(); | |
| 48 | + | m_startPhase = fin->get8(); | |
| 49 | + | ||
| 50 | + | for (int i = 0; i < m_animationPhases; ++i) { | |
| 51 | + | int minimum = fin->getU32(); | |
| 52 | + | int maximum = fin->getU32(); | |
| 53 | + | m_phaseDurations.push_back( | |
| 54 | + | std::make_pair(minimum, std::max(0, maximum - minimum))); | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | m_phase = getStartPhase(); | |
| 58 | + | ||
| 59 | + | VALIDATE(m_animationPhases == (int)m_phaseDurations.size()); | |
| 60 | + | VALIDATE(m_startPhase >= -1 && m_startPhase < m_animationPhases); | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | void Animator::serialize(const FileStreamPtr &fin) { | |
| 64 | + | fin->addU8(m_async ? 0 : 1); | |
| 65 | + | fin->add32(m_loopCount); | |
| 66 | + | fin->add8(m_startPhase); | |
| 67 | + | ||
| 68 | + | for (auto &phase : m_phaseDurations) { | |
| 69 | + | fin->addU32(phase.first); | |
| 70 | + | fin->addU32(phase.first + phase.second); | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | void Animator::setPhase(int phase) { | |
| 75 | + | if (m_phase == phase && !m_isComplete) | |
| 76 | + | return; | |
| 77 | + | ||
| 78 | + | if (m_async) { | |
| 79 | + | if (phase == AnimPhaseAsync) | |
| 80 | + | m_phase = 0; | |
| 81 | + | else if (phase == AnimPhaseRandom) | |
| 82 | + | m_phase = (int)stdext::random_range(0, (long)m_animationPhases); | |
| 83 | + | else if (phase >= 0 && phase < m_animationPhases) | |
| 84 | + | m_phase = phase; | |
| 85 | + | else | |
| 86 | + | m_phase = getStartPhase(); | |
| 87 | + | ||
| 88 | + | m_isComplete = false; | |
| 89 | + | m_lastPhaseTicks = g_clock.millis(); | |
| 90 | + | m_currentDuration = getPhaseDuration(phase); | |
| 91 | + | m_currentLoop = 0; | |
| 92 | + | } else | |
| 93 | + | calculateSynchronous(); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | int Animator::getPhase() { | |
| 97 | + | ticks_t ticks = g_clock.millis(); | |
| 98 | + | if (ticks != m_lastPhaseTicks && !m_isComplete) { | |
| 99 | + | int elapsedTicks = (int)(ticks - m_lastPhaseTicks); | |
| 100 | + | if (elapsedTicks >= m_currentDuration) { | |
| 101 | + | int phase = m_phase; | |
| 102 | + | while (elapsedTicks >= m_currentDuration) { | |
| 103 | + | elapsedTicks -= m_currentDuration; | |
| 104 | + | if (m_loopCount < 0) | |
| 105 | + | phase = getPingPongPhase(); | |
| 106 | + | else | |
| 107 | + | phase = getLoopPhase(); | |
| 108 | + | ||
| 109 | + | if (m_phase != phase) { | |
| 110 | + | m_phase = phase; | |
| 111 | + | m_currentDuration = getPhaseDuration(phase); | |
| 112 | + | } else { | |
| 113 | + | m_isComplete = true; | |
| 114 | + | break; | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | if (!m_isComplete) | |
| 118 | + | m_currentDuration -= elapsedTicks; | |
| 119 | + | } else { | |
| 120 | + | m_currentDuration -= elapsedTicks; | |
| 121 | + | } | |
| 122 | + | m_lastPhaseTicks = ticks; | |
| 123 | + | } | |
| 124 | + | return m_phase; | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | int Animator::getPhaseAt(Timer &timer, int lastPhase) { | |
| 128 | + | static int rand_val = 6; | |
| 129 | + | ticks_t time = timer.ticksElapsed(); | |
| 130 | + | for (int i = lastPhase; i < m_animationPhases; ++i) { | |
| 131 | + | int phaseDuration = m_phaseDurations[i].second == 0 | |
| 132 | + | ? m_phaseDurations[i].first | |
| 133 | + | : m_phaseDurations[i].first + | |
| 134 | + | rand_val % (m_phaseDurations[i].second); | |
| 135 | + | rand_val = rand_val * 7 + 11; | |
| 136 | + | if (time < phaseDuration) { | |
| 137 | + | timer.restart(); | |
| 138 | + | timer.adjust(-time); | |
| 139 | + | return i; | |
| 140 | + | } | |
| 141 | + | time -= phaseDuration; | |
| 142 | + | } | |
| 143 | + | return 0; | |
| 144 | + | /* | |
| 145 | + | ticks_t total = 0; | |
| 146 | + | ||
| 147 | + | for (const auto &pair : m_phaseDurations) { | |
| 148 | + | total += std::get<1>(pair); | |
| 149 | + | ||
| 150 | + | if (time < total) { | |
| 151 | + | return index; | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | ++index; | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | return std::min<int>(index, m_animationPhases - 1); | |
| 158 | + | */ | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | int Animator::getStartPhase() { | |
| 162 | + | if (m_startPhase > -1) | |
| 163 | + | return m_startPhase; | |
| 164 | + | return (int)stdext::random_range(0, (long)m_animationPhases); | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | void Animator::resetAnimation() { | |
| 168 | + | m_isComplete = false; | |
| 169 | + | m_currentDirection = AnimDirForward; | |
| 170 | + | m_currentLoop = 0; | |
| 171 | + | setPhase(AnimPhaseAutomatic); | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | int Animator::getPingPongPhase() { | |
| 175 | + | int count = m_currentDirection == AnimDirForward ? 1 : -1; | |
| 176 | + | int nextPhase = m_phase + count; | |
| 177 | + | if (nextPhase < 0 || nextPhase >= m_animationPhases) { | |
| 178 | + | m_currentDirection = | |
| 179 | + | m_currentDirection == AnimDirForward ? AnimDirBackward : AnimDirForward; | |
| 180 | + | count *= -1; | |
| 181 | + | } | |
| 182 | + | return m_phase + count; | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | int Animator::getLoopPhase() { | |
| 186 | + | int nextPhase = m_phase + 1; | |
| 187 | + | if (nextPhase < m_animationPhases) | |
| 188 | + | return nextPhase; | |
| 189 | + | ||
| 190 | + | if (m_loopCount == 0) | |
| 191 | + | return 0; | |
| 192 | + | ||
| 193 | + | if (m_currentLoop < (m_loopCount - 1)) { | |
| 194 | + | m_currentLoop++; | |
| 195 | + | return 0; | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | return m_phase; | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | int Animator::getPhaseDuration(int phase) { | |
| 202 | + | VALIDATE(phase < (int)m_phaseDurations.size()); | |
| 203 | + | ||
| 204 | + | auto &data = m_phaseDurations.at(phase); | |
| 205 | + | if (data.second == 0) | |
| 206 | + | return data.first; | |
| 207 | + | int min = data.first; | |
| 208 | + | int max = min + data.second; | |
| 209 | + | return (int)stdext::random_range((long)min, (long)max); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | void Animator::calculateSynchronous() { | |
| 213 | + | m_isComplete = false; | |
| 214 | + | int totalDuration = 0; | |
| 215 | + | for (int i = 0; i < m_animationPhases; i++) | |
| 216 | + | totalDuration += getPhaseDuration(i); | |
| 217 | + | ||
| 218 | + | ticks_t ticks = g_clock.millis(); | |
| 219 | + | int elapsedTicks = (int)(ticks % totalDuration); | |
| 220 | + | int totalTime = 0; | |
| 221 | + | for (int i = 0; i < m_animationPhases; i++) { | |
| 222 | + | int duration = getPhaseDuration(i); | |
| 223 | + | if (elapsedTicks >= totalTime && elapsedTicks < totalTime + duration) { | |
| 224 | + | m_phase = i; | |
| 225 | + | m_currentDuration = duration - (elapsedTicks - totalTime); | |
| 226 | + | break; | |
| 227 | + | } | |
| 228 | + | totalTime += duration; | |
| 229 | + | } | |
| 230 | + | m_lastPhaseTicks = ticks; | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | ticks_t Animator::getTotalDuration() { | |
| 234 | + | ticks_t time = 0; | |
| 235 | + | for (const auto &pair : m_phaseDurations) { | |
| 236 | + | time += pair.first + pair.second; | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | return time; | |
| 240 | + | } |
| @@ -0,0 +1,87 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #ifndef ANIMATOR_H | |
| 24 | + | #define ANIMATOR_H | |
| 25 | + | ||
| 26 | + | #include "declarations.h" | |
| 27 | + | ||
| 28 | + | #include <framework/core/declarations.h> | |
| 29 | + | #include <framework/core/timer.h> | |
| 30 | + | ||
| 31 | + | enum AnimationPhase : int16 | |
| 32 | + | { | |
| 33 | + | AnimPhaseAutomatic = -1, | |
| 34 | + | AnimPhaseRandom = 254, | |
| 35 | + | AnimPhaseAsync = 255, | |
| 36 | + | }; | |
| 37 | + | ||
| 38 | + | enum AnimationDirection : uint8 | |
| 39 | + | { | |
| 40 | + | AnimDirForward = 0, | |
| 41 | + | AnimDirBackward = 1 | |
| 42 | + | }; | |
| 43 | + | ||
| 44 | + | class Animator : public stdext::shared_object | |
| 45 | + | { | |
| 46 | + | public: | |
| 47 | + | Animator(); | |
| 48 | + | ||
| 49 | + | void unserialize(int animationPhases, const FileStreamPtr& fin); | |
| 50 | + | void serialize(const FileStreamPtr& fin); | |
| 51 | + | ||
| 52 | + | void setPhase(int phase); | |
| 53 | + | int getPhase(); | |
| 54 | + | int getPhaseAt(Timer& timer, int lastPhase = 0); | |
| 55 | + | ||
| 56 | + | int getStartPhase(); | |
| 57 | + | int getAnimationPhases() { return m_animationPhases; } | |
| 58 | + | bool isAsync() { return m_async; } | |
| 59 | + | bool isComplete() { return m_isComplete; } | |
| 60 | + | ||
| 61 | + | ticks_t getTotalDuration(); | |
| 62 | + | ||
| 63 | + | void resetAnimation(); | |
| 64 | + | ||
| 65 | + | private: | |
| 66 | + | int getPingPongPhase(); | |
| 67 | + | int getLoopPhase(); | |
| 68 | + | int getPhaseDuration(int phase); | |
| 69 | + | void calculateSynchronous(); | |
| 70 | + | ||
| 71 | + | int m_animationPhases; | |
| 72 | + | int m_startPhase; | |
| 73 | + | int m_loopCount; | |
| 74 | + | bool m_async; | |
| 75 | + | std::vector< std::pair<int, int> > m_phaseDurations; | |
| 76 | + | ||
| 77 | + | int m_currentDuration; | |
| 78 | + | AnimationDirection m_currentDirection; | |
| 79 | + | int m_currentLoop; | |
| 80 | + | ||
| 81 | + | ticks_t m_lastPhaseTicks; | |
| 82 | + | bool m_isComplete; | |
| 83 | + | ||
| 84 | + | int m_phase; | |
| 85 | + | }; | |
| 86 | + | ||
| 87 | + | #endif |
| @@ -0,0 +1,60 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #include "client.h" | |
| 24 | + | #include <framework/core/modulemanager.h> | |
| 25 | + | #include <framework/core/resourcemanager.h> | |
| 26 | + | #include <framework/graphics/graphics.h> | |
| 27 | + | #include <framework/graphics/shadermanager.h> | |
| 28 | + | #include "game.h" | |
| 29 | + | #include "map.h" | |
| 30 | + | #include "spritemanager.h" | |
| 31 | + | #include "minimap.h" | |
| 32 | + | #include "healthbars.h" | |
| 33 | + | #include <framework/core/configmanager.h> | |
| 34 | + | ||
| 35 | + | Client g_client; | |
| 36 | + | ||
| 37 | + | void Client::init(std::vector<std::string>& args) | |
| 38 | + | { | |
| 39 | + | // register needed lua functions | |
| 40 | + | registerLuaFunctions(); | |
| 41 | + | ||
| 42 | + | g_map.init(); | |
| 43 | + | g_minimap.init(); | |
| 44 | + | g_game.init(); | |
| 45 | + | g_shaders.init(); | |
| 46 | + | g_things.init(); | |
| 47 | + | g_healthBars.init(); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | void Client::terminate() | |
| 51 | + | { | |
| 52 | + | g_creatures.terminate(); | |
| 53 | + | g_game.terminate(); | |
| 54 | + | g_map.terminate(); | |
| 55 | + | g_minimap.terminate(); | |
| 56 | + | g_things.terminate(); | |
| 57 | + | g_sprites.terminate(); | |
| 58 | + | g_shaders.terminate(); | |
| 59 | + | g_healthBars.terminate(); | |
| 60 | + | } |
| @@ -0,0 +1,38 @@ | |||
| 1 | + | /* | |
| 2 | + | * Copyright (c) 2010-2017 OTClient <https://github.com/edubart/otclient> | |
| 3 | + | * | |
| 4 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 5 | + | * of this software and associated documentation files (the "Software"), to deal | |
| 6 | + | * in the Software without restriction, including without limitation the rights | |
| 7 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 8 | + | * copies of the Software, and to permit persons to whom the Software is | |
| 9 | + | * furnished to do so, subject to the following conditions: | |
| 10 | + | * | |
| 11 | + | * The above copyright notice and this permission notice shall be included in | |
| 12 | + | * all copies or substantial portions of the Software. | |
| 13 | + | * | |
| 14 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 15 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 16 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 17 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 18 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 19 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| 20 | + | * THE SOFTWARE. | |
| 21 | + | */ | |
| 22 | + | ||
| 23 | + | #ifndef CLIENT_H | |
| 24 | + | #define CLIENT_H | |
| 25 | + | ||
| 26 | + | #include "global.h" | |
| 27 | + | ||
| 28 | + | class Client | |
| 29 | + | { | |
| 30 | + | public: | |
| 31 | + | void init(std::vector<std::string>& args); | |
| 32 | + | void terminate(); | |
| 33 | + | void registerLuaFunctions(); | |
| 34 | + | }; | |
| 35 | + | ||
| 36 | + | extern Client g_client; | |
| 37 | + | ||
| 38 | + | #endif |
| @@ -0,0 +1,99 @@ | |||
| 1 | + | add_definitions(-DCLIENT) | |
| 2 | + | ||
| 3 | + | set(client_SOURCES ${client_SOURCES} | |
| 4 | + | # client | |
| 5 | + | ${CMAKE_CURRENT_LIST_DIR}/const.h | |
| 6 | + | ${CMAKE_CURRENT_LIST_DIR}/global.h | |
| 7 | + | ${CMAKE_CURRENT_LIST_DIR}/luafunctions_client.cpp | |
| 8 | + | ${CMAKE_CURRENT_LIST_DIR}/client.cpp | |
| 9 | + | ${CMAKE_CURRENT_LIST_DIR}/client.h | |
| 10 | + | ||
| 11 | + | # core | |
| 12 | + | ${CMAKE_CURRENT_LIST_DIR}/animatedtext.cpp | |
| 13 | + | ${CMAKE_CURRENT_LIST_DIR}/animatedtext.h | |
| 14 | + | ${CMAKE_CURRENT_LIST_DIR}/animator.h | |
| 15 | + | ${CMAKE_CURRENT_LIST_DIR}/animator.cpp | |
| 16 | + | ${CMAKE_CURRENT_LIST_DIR}/container.cpp | |
| 17 | + | ${CMAKE_CURRENT_LIST_DIR}/container.h | |
| 18 | + | ${CMAKE_CURRENT_LIST_DIR}/creature.cpp | |
| 19 | + | ${CMAKE_CURRENT_LIST_DIR}/creature.h | |
| 20 | + | ${CMAKE_CURRENT_LIST_DIR}/declarations.h | |
| 21 | + | ${CMAKE_CURRENT_LIST_DIR}/effect.cpp | |
| 22 | + | ${CMAKE_CURRENT_LIST_DIR}/effect.h | |
| 23 | + | ${CMAKE_CURRENT_LIST_DIR}/game.cpp | |
| 24 | + | ${CMAKE_CURRENT_LIST_DIR}/game.h | |
| 25 | + | ${CMAKE_CURRENT_LIST_DIR}/item.cpp | |
| 26 | + | ${CMAKE_CURRENT_LIST_DIR}/item.h | |
| 27 | + | ${CMAKE_CURRENT_LIST_DIR}/localplayer.cpp | |
| 28 | + | ${CMAKE_CURRENT_LIST_DIR}/localplayer.h | |
| 29 | + | ${CMAKE_CURRENT_LIST_DIR}/map.cpp | |
| 30 | + | ${CMAKE_CURRENT_LIST_DIR}/map.h | |
| 31 | + | ${CMAKE_CURRENT_LIST_DIR}/mapio.cpp | |
| 32 | + | ${CMAKE_CURRENT_LIST_DIR}/mapview.cpp | |
| 33 | + | ${CMAKE_CURRENT_LIST_DIR}/mapview.h | |
| 34 | + | ${CMAKE_CURRENT_LIST_DIR}/minimap.cpp | |
| 35 | + | ${CMAKE_CURRENT_LIST_DIR}/minimap.h | |
| 36 | + | ${CMAKE_CURRENT_LIST_DIR}/lightview.cpp | |
| 37 | + | ${CMAKE_CURRENT_LIST_DIR}/lightview.h | |
| 38 | + | ${CMAKE_CURRENT_LIST_DIR}/missile.cpp | |
| 39 | + | ${CMAKE_CURRENT_LIST_DIR}/missile.h | |
| 40 | + | ${CMAKE_CURRENT_LIST_DIR}/outfit.cpp | |
| 41 | + | ${CMAKE_CURRENT_LIST_DIR}/outfit.h | |
| 42 | + | ${CMAKE_CURRENT_LIST_DIR}/player.cpp | |
| 43 | + | ${CMAKE_CURRENT_LIST_DIR}/player.h | |
| 44 | + | ${CMAKE_CURRENT_LIST_DIR}/spritemanager.cpp | |
| 45 | + | ${CMAKE_CURRENT_LIST_DIR}/spritemanager.h | |
| 46 | + | ${CMAKE_CURRENT_LIST_DIR}/statictext.cpp | |
| 47 | + | ${CMAKE_CURRENT_LIST_DIR}/statictext.h | |
| 48 | + | ${CMAKE_CURRENT_LIST_DIR}/thing.cpp | |
| 49 | + | ${CMAKE_CURRENT_LIST_DIR}/thing.h | |
| 50 | + | ${CMAKE_CURRENT_LIST_DIR}/thingtypemanager.cpp | |
| 51 | + | ${CMAKE_CURRENT_LIST_DIR}/thingtypemanager.h | |
| 52 | + | ${CMAKE_CURRENT_LIST_DIR}/thingtype.cpp | |
| 53 | + | ${CMAKE_CURRENT_LIST_DIR}/thingtype.h | |
| 54 | + | ${CMAKE_CURRENT_LIST_DIR}/itemtype.cpp | |
| 55 | + | ${CMAKE_CURRENT_LIST_DIR}/itemtype.h | |
| 56 | + | ${CMAKE_CURRENT_LIST_DIR}/tile.cpp | |
| 57 | + | ${CMAKE_CURRENT_LIST_DIR}/tile.h | |
| 58 | + | ${CMAKE_CURRENT_LIST_DIR}/healthbars.cpp | |
| 59 | + | ${CMAKE_CURRENT_LIST_DIR}/healthbars.h | |
| 60 | + | ${CMAKE_CURRENT_LIST_DIR}/houses.cpp | |
| 61 | + | ${CMAKE_CURRENT_LIST_DIR}/houses.h | |
| 62 | + | ${CMAKE_CURRENT_LIST_DIR}/towns.cpp | |
| 63 | + | ${CMAKE_CURRENT_LIST_DIR}/towns.h | |
| 64 | + | ${CMAKE_CURRENT_LIST_DIR}/creatures.cpp | |
| 65 | + | ${CMAKE_CURRENT_LIST_DIR}/creatures.h | |
| 66 | + | ||
| 67 | + | # lua | |
| 68 | + | ${CMAKE_CURRENT_LIST_DIR}/luavaluecasts_client.cpp | |
| 69 | + | ${CMAKE_CURRENT_LIST_DIR}/luavaluecasts_client.h | |
| 70 | + | ||
| 71 | + | # net | |
| 72 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolcodes.cpp | |
| 73 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolcodes.h | |
| 74 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolgame.cpp | |
| 75 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolgame.h | |
| 76 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolgameparse.cpp | |
| 77 | + | ${CMAKE_CURRENT_LIST_DIR}/protocolgamesend.cpp | |
| 78 | + | ||
| 79 | + | # ui | |
| 80 | + | ${CMAKE_CURRENT_LIST_DIR}/uicreature.cpp | |
| 81 | + | ${CMAKE_CURRENT_LIST_DIR}/uicreature.h | |
| 82 | + | ${CMAKE_CURRENT_LIST_DIR}/uiitem.cpp | |
| 83 | + | ${CMAKE_CURRENT_LIST_DIR}/uiitem.h | |
| 84 | + | ${CMAKE_CURRENT_LIST_DIR}/uigraph.cpp | |
| 85 | + | ${CMAKE_CURRENT_LIST_DIR}/uigraph.h | |
| 86 | + | ${CMAKE_CURRENT_LIST_DIR}/uimap.cpp | |
| 87 | + | ${CMAKE_CURRENT_LIST_DIR}/uimap.h | |
| 88 | + | ${CMAKE_CURRENT_LIST_DIR}/uiminimap.cpp | |
| 89 | + | ${CMAKE_CURRENT_LIST_DIR}/uiminimap.h | |
| 90 | + | ${CMAKE_CURRENT_LIST_DIR}/uiprogressrect.cpp | |
| 91 | + | ${CMAKE_CURRENT_LIST_DIR}/uiprogressrect.h | |
| 92 | + | ${CMAKE_CURRENT_LIST_DIR}/uimapanchorlayout.cpp | |
| 93 | + | ${CMAKE_CURRENT_LIST_DIR}/uimapanchorlayout.h | |
| 94 | + | ${CMAKE_CURRENT_LIST_DIR}/uisprite.cpp | |
| 95 | + | ${CMAKE_CURRENT_LIST_DIR}/uisprite.h | |
| 96 | + | ||
| 97 | + | # util | |
| 98 | + | ${CMAKE_CURRENT_LIST_DIR}/position.h | |
| 99 | + | ) |