mirror of
https://github.com/libsdl-org/SDL.git
synced 2024-11-23 10:53:27 +08:00
4f55271571
It was intended to make the API easier to use, but various automatic garbage collection all had flaws, and making the application periodically clean up temporary memory added cognitive load to using the API, and in many cases was it was difficult to restructure threaded code to handle this. So, we're largely going back to the original system, where the API returns allocated results and you free them. In addition, to solve the problems we originally wanted temporary memory for: * Short strings with a finite count, like device names, get stored in a per-thread string pool. * Events continue to use temporary memory internally, which is cleaned up on the next event processing cycle.
527 lines
18 KiB
C
527 lines
18 KiB
C
/*
|
|
Simple DirectMedia Layer
|
|
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
|
|
|
|
This software is provided 'as-is', without any express or implied
|
|
warranty. In no event will the authors be held liable for any damages
|
|
arising from the use of this software.
|
|
|
|
Permission is granted to anyone to use this software for any purpose,
|
|
including commercial applications, and to alter it and redistribute it
|
|
freely, subject to the following restrictions:
|
|
|
|
1. The origin of this software must not be misrepresented; you must not
|
|
claim that you wrote the original software. If you use this software
|
|
in a product, an acknowledgment in the product documentation would be
|
|
appreciated but is not required.
|
|
2. Altered source versions must be plainly marked as such, and must not be
|
|
misrepresented as being the original software.
|
|
3. This notice may not be removed or altered from any source distribution.
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_main.h>
|
|
#include <SDL3/SDL_test.h>
|
|
|
|
#define WIDTH 1600
|
|
#define HEIGHT 1200
|
|
|
|
#define VERBOSE 0
|
|
|
|
#define ALWAYS_SHOW_PRESSURE_BOX 1
|
|
|
|
static SDLTest_CommonState *state;
|
|
static int quitting = 0;
|
|
|
|
static float last_x, last_y;
|
|
static float last_xtilt, last_ytilt, last_pressure, last_distance, last_rotation;
|
|
static int last_button;
|
|
static int last_touching; /* tip touches surface */
|
|
static int last_was_eraser;
|
|
|
|
static SDL_Texture *offscreen_texture = NULL;
|
|
|
|
static void DrawScreen(SDL_Renderer *renderer)
|
|
{
|
|
float xdelta, ydelta, endx, endy;
|
|
/* off-screen texture to render into */
|
|
SDL_Texture *window_texture;
|
|
const float X = 128.0f, Y = 128.0f; /* mid-point in the off-screen texture */
|
|
SDL_FRect dest_rect;
|
|
float tilt_vec_x = SDL_sinf(last_xtilt * SDL_PI_F / 180.0f);
|
|
float tilt_vec_y = SDL_sinf(last_ytilt * SDL_PI_F / 180.0f);
|
|
int color = last_button + 1;
|
|
|
|
if (!renderer) {
|
|
return;
|
|
}
|
|
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff);
|
|
SDL_RenderClear(renderer);
|
|
|
|
if (offscreen_texture == NULL) {
|
|
offscreen_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, (int)(X * 2.0f), (int)(Y * 2.0f));
|
|
}
|
|
|
|
/* Render into off-screen texture so we can do pixel-precise rendering later */
|
|
window_texture = SDL_GetRenderTarget(renderer);
|
|
SDL_SetRenderTarget(renderer, offscreen_texture);
|
|
|
|
/* Rendering starts here */
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff);
|
|
SDL_RenderClear(renderer);
|
|
|
|
SDL_SetRenderDrawColor(renderer, 0xa0, 0xa0, 0xa0, 0xff);
|
|
if (last_touching) {
|
|
SDL_FRect rect;
|
|
|
|
rect.x = 0;
|
|
rect.y = 0;
|
|
rect.w = 2.0f * X - 1.0f;
|
|
rect.h = 2.0f * Y - 1.0f;
|
|
|
|
SDL_RenderRect(renderer, &rect);
|
|
} else {
|
|
/* Show where the pen is rotating when it isn't touching the surface.
|
|
Otherwise we draw the rotation angle below together with pressure information. */
|
|
float rot_vecx = SDL_sinf(last_rotation / 180.0f * SDL_PI_F);
|
|
float rot_vecy = -SDL_cosf(last_rotation / 180.0f * SDL_PI_F);
|
|
float px = X + rot_vecx * 100.0f;
|
|
float py = Y + rot_vecy * 100.0f;
|
|
float px2 = X + rot_vecx * 80.0f;
|
|
float py2 = Y + rot_vecy * 80.0f;
|
|
|
|
SDL_RenderLine(renderer,
|
|
px, py,
|
|
px2 + rot_vecy * 20.0f,
|
|
py2 - rot_vecx * 20.0f);
|
|
SDL_RenderLine(renderer,
|
|
px, py,
|
|
px2 - rot_vecy * 20.0f,
|
|
py2 + rot_vecx * 20.0f);
|
|
}
|
|
|
|
if (last_was_eraser) {
|
|
SDL_FRect rect;
|
|
|
|
rect.x = X - 10.0f;
|
|
rect.y = Y - 10.0f;
|
|
rect.w = 21.0f;
|
|
rect.h = 21.0f;
|
|
|
|
SDL_SetRenderDrawColor(renderer, 0x00, 0xff, 0xff, 0xff);
|
|
SDL_RenderFillRect(renderer, &rect);
|
|
} else {
|
|
float distance = last_distance * 50.0f;
|
|
|
|
SDL_SetRenderDrawColor(renderer, 0xff, 0, 0, 0xff);
|
|
SDL_RenderLine(renderer,
|
|
X - 10.0f - distance, Y,
|
|
X - distance, Y);
|
|
SDL_RenderLine(renderer,
|
|
X + 10.0f + distance, Y,
|
|
X + distance, Y);
|
|
SDL_RenderLine(renderer,
|
|
X, Y - 10.0f - distance,
|
|
X, Y - distance);
|
|
SDL_RenderLine(renderer,
|
|
X, Y + 10.0f + distance,
|
|
X, Y + distance);
|
|
|
|
}
|
|
|
|
/* Draw a cone based on the direction the pen is leaning as if it were shining a light. */
|
|
/* Colour derived from pens, intensity based on pressure: */
|
|
SDL_SetRenderDrawColor(renderer,
|
|
(color & 0x01) ? 0xff : 0,
|
|
(color & 0x02) ? 0xff : 0,
|
|
(color & 0x04) ? 0xff : 0,
|
|
(int)(0xff));
|
|
|
|
xdelta = -tilt_vec_x * 100.0f;
|
|
ydelta = -tilt_vec_y * 100.0f;
|
|
endx = X + xdelta;
|
|
endy = Y + ydelta;
|
|
SDL_RenderLine(renderer, X, Y, endx, endy);
|
|
|
|
SDL_SetRenderDrawColor(renderer,
|
|
(color & 0x01) ? 0xff : 0,
|
|
(color & 0x02) ? 0xff : 0,
|
|
(color & 0x04) ? 0xff : 0,
|
|
(Uint8)(0xff * last_pressure));
|
|
/* Cone base width based on pressure: */
|
|
SDL_RenderLine(renderer, X, Y, endx + (ydelta * last_pressure / 3.0f), endy - (xdelta * last_pressure / 3.0f));
|
|
SDL_RenderLine(renderer, X, Y, endx - (ydelta * last_pressure / 3.0f), endy + (xdelta * last_pressure / 3.0f));
|
|
|
|
/* If tilt is very small (or zero, for pens that don't have tilt), add some extra lines, rotated by the current rotation value */
|
|
if (ALWAYS_SHOW_PRESSURE_BOX || (SDL_fabsf(tilt_vec_x) < 0.2f && SDL_fabsf(tilt_vec_y) < 0.2f)) {
|
|
int rot;
|
|
float pressure = last_pressure * 80.0f;
|
|
|
|
/* Four times, rotated 90 degrees, so that we get a box */
|
|
for (rot = 0; rot < 4; ++rot) {
|
|
|
|
float vecx = SDL_cosf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F);
|
|
float vecy = SDL_sinf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F);
|
|
|
|
float px = X + vecx * pressure;
|
|
float py = Y + vecy * pressure;
|
|
|
|
SDL_RenderLine(renderer,
|
|
px + vecy * 10.0f, py - vecx * 10.0f,
|
|
px - vecy * 10.0f, py + vecx * 10.0f);
|
|
|
|
if (rot == 3) {
|
|
int r = 0;
|
|
for (; r >= 0; r -= 2) {
|
|
float delta = 10.0f - ((float) r);
|
|
|
|
SDL_RenderLine(renderer,
|
|
px + vecy * delta, py - vecx * delta,
|
|
px + (vecx * pressure * 0.4f),
|
|
py + (vecy * pressure * 0.4f));
|
|
SDL_RenderLine(renderer,
|
|
px - vecy * delta, py + vecx * delta,
|
|
px + (vecx * pressure * 0.4f),
|
|
py + (vecy * pressure * 0.4f));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SDL_SetRenderTarget(renderer, window_texture);
|
|
/* Now render to pixel-precise position */
|
|
dest_rect.x = last_x - X;
|
|
dest_rect.y = last_y - Y;
|
|
dest_rect.w = X * 2.0f;
|
|
dest_rect.h = Y * 2.0f;
|
|
SDL_RenderTexture(renderer, offscreen_texture, NULL, &dest_rect);
|
|
SDL_RenderPresent(renderer);
|
|
}
|
|
|
|
static void dump_state(void)
|
|
{
|
|
int i;
|
|
int pens_nr;
|
|
|
|
/* Make sure this also works with a NULL parameter */
|
|
SDL_PenID* pens = SDL_GetPens(NULL);
|
|
if (pens) {
|
|
SDL_free(pens);
|
|
}
|
|
|
|
pens = SDL_GetPens(&pens_nr);
|
|
if (!pens) {
|
|
SDL_Log("Couldn't get pens: %s\n", SDL_GetError());
|
|
return;
|
|
}
|
|
SDL_Log("Found %d pens (terminated by %u)\n", pens_nr, (unsigned) pens[pens_nr]);
|
|
|
|
for (i = 0; i < pens_nr; ++i) {
|
|
SDL_PenID penid = pens[i];
|
|
SDL_GUID guid = SDL_GetPenGUID(penid);
|
|
char guid_str[33];
|
|
float axes[SDL_PEN_NUM_AXES];
|
|
float x, y;
|
|
int k;
|
|
SDL_PenCapabilityInfo info;
|
|
Uint32 status = SDL_GetPenStatus(penid, &x, &y, axes, SDL_PEN_NUM_AXES);
|
|
const SDL_PenCapabilityFlags capabilities = SDL_GetPenCapabilities(penid, &info);
|
|
char *type;
|
|
char *buttons_str;
|
|
|
|
SDL_GUIDToString(guid, guid_str, sizeof(guid_str));
|
|
|
|
switch (SDL_GetPenType(penid)) {
|
|
case SDL_PEN_TYPE_ERASER:
|
|
type = "Eraser";
|
|
break;
|
|
case SDL_PEN_TYPE_PEN:
|
|
type = "Pen";
|
|
break;
|
|
case SDL_PEN_TYPE_PENCIL:
|
|
type = "Pencil";
|
|
break;
|
|
case SDL_PEN_TYPE_BRUSH:
|
|
type = "Brush";
|
|
break;
|
|
case SDL_PEN_TYPE_AIRBRUSH:
|
|
type = "Airbrush";
|
|
break;
|
|
default:
|
|
type = "Unknown (bug?)";
|
|
}
|
|
|
|
switch (info.num_buttons) {
|
|
case SDL_PEN_INFO_UNKNOWN:
|
|
SDL_asprintf(&buttons_str, "? buttons");
|
|
break;
|
|
case 1:
|
|
SDL_asprintf(&buttons_str, "1 button");
|
|
break;
|
|
default:
|
|
SDL_asprintf(&buttons_str, "%d button", info.num_buttons);
|
|
break;
|
|
}
|
|
|
|
SDL_Log("%s %lu: [%s] attached=%d, %s [cap= %08lx:%08lx =status] '%s'\n",
|
|
type,
|
|
(unsigned long) penid, guid_str,
|
|
SDL_PenConnected(penid), /* should always be SDL_TRUE during iteration */
|
|
buttons_str,
|
|
(unsigned long) capabilities,
|
|
(unsigned long) status,
|
|
SDL_GetPenName(penid));
|
|
SDL_free(buttons_str);
|
|
SDL_Log(" pos=(%.2f, %.2f)", x, y);
|
|
for (k = 0; k < SDL_PEN_NUM_AXES; ++k) {
|
|
SDL_bool supported = ((capabilities & SDL_PEN_AXIS_CAPABILITY(k)) != 0);
|
|
if (supported) {
|
|
if (k == SDL_PEN_AXIS_XTILT || k == SDL_PEN_AXIS_YTILT) {
|
|
if (info.max_tilt == SDL_PEN_INFO_UNKNOWN) {
|
|
SDL_Log(" axis %d: %.3f (max tilt unknown)", k, axes[k]);
|
|
} else {
|
|
SDL_Log(" axis %d: %.3f (tilt -%.1f..%.1f)", k, axes[k],
|
|
info.max_tilt, info.max_tilt);
|
|
}
|
|
} else {
|
|
SDL_Log(" axis %d: %.3f", k, axes[k]);
|
|
}
|
|
} else {
|
|
SDL_Log(" axis %d: unsupported (%.3f)", k, axes[k]);
|
|
}
|
|
}
|
|
}
|
|
SDL_free(pens);
|
|
}
|
|
|
|
static void update_axes(float *axes)
|
|
{
|
|
last_xtilt = axes[SDL_PEN_AXIS_XTILT];
|
|
last_ytilt = axes[SDL_PEN_AXIS_YTILT];
|
|
last_pressure = axes[SDL_PEN_AXIS_PRESSURE];
|
|
last_distance = axes[SDL_PEN_AXIS_DISTANCE];
|
|
last_rotation = axes[SDL_PEN_AXIS_ROTATION];
|
|
}
|
|
|
|
static void update_axes_from_touch(const float pressure)
|
|
{
|
|
last_xtilt = 0;
|
|
last_ytilt = 0;
|
|
last_pressure = pressure;
|
|
last_distance = 0;
|
|
last_rotation = 0;
|
|
}
|
|
|
|
static void process_event(SDL_Event event)
|
|
{
|
|
SDLTest_CommonEvent(state, &event, &quitting);
|
|
|
|
switch (event.type) {
|
|
case SDL_EVENT_KEY_DOWN:
|
|
{
|
|
dump_state();
|
|
break;
|
|
}
|
|
case SDL_EVENT_MOUSE_MOTION:
|
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
|
#if VERBOSE
|
|
{
|
|
float x, y;
|
|
SDL_GetMouseState(&x, &y);
|
|
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
|
SDL_Log("[%lu] mouse motion: mouse ID %d is at (%.2f, %.2f) (state: %.2f,%.2f) delta (%.2f, %.2f)\n",
|
|
event.motion.timestamp,
|
|
event.motion.which,
|
|
event.motion.x, event.motion.y,
|
|
event.motion.xrel, event.motion.yrel,
|
|
x, y);
|
|
} else {
|
|
SDL_Log("[%lu] mouse button: mouse ID %d is at (%.2f, %.2f) (state: %.2f,%.2f)\n",
|
|
event.button.timestamp,
|
|
event.button.which,
|
|
event.button.x, event.button.y,
|
|
x, y);
|
|
}
|
|
}
|
|
#endif
|
|
if (event.motion.which != SDL_PEN_MOUSEID && event.motion.which != SDL_TOUCH_MOUSEID) {
|
|
SDL_ShowCursor();
|
|
} break;
|
|
|
|
case SDL_EVENT_PEN_MOTION:
|
|
{
|
|
SDL_PenMotionEvent *ev = &event.pmotion;
|
|
|
|
SDL_HideCursor();
|
|
last_x = ev->x;
|
|
last_y = ev->y;
|
|
update_axes(ev->axes);
|
|
last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK;
|
|
#if VERBOSE
|
|
SDL_Log("[%lu] pen motion: %s %u at (%.4f, %.4f); pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f [buttons=%02x]\n",
|
|
(unsigned long) ev->timestamp,
|
|
last_was_eraser ? "eraser" : "pen",
|
|
(unsigned int)ev->which, ev->x, ev->y, last_pressure, last_xtilt, last_ytilt, last_distance,
|
|
ev->pen_state);
|
|
#endif
|
|
} break;
|
|
|
|
case SDL_EVENT_PEN_UP:
|
|
case SDL_EVENT_PEN_DOWN: {
|
|
SDL_PenTipEvent *ev = &event.ptip;
|
|
last_x = ev->x;
|
|
last_y = ev->y;
|
|
update_axes(ev->axes);
|
|
last_was_eraser = ev->tip == SDL_PEN_TIP_ERASER;
|
|
last_button = ev->pen_state & 0xf; /* button mask */
|
|
last_touching = (event.type == SDL_EVENT_PEN_DOWN);
|
|
} break;
|
|
|
|
case SDL_EVENT_PEN_BUTTON_UP:
|
|
case SDL_EVENT_PEN_BUTTON_DOWN:
|
|
{
|
|
SDL_PenButtonEvent *ev = &event.pbutton;
|
|
|
|
SDL_HideCursor();
|
|
last_x = ev->x;
|
|
last_y = ev->y;
|
|
update_axes(ev->axes);
|
|
if (last_pressure > 0.0f && !last_touching) {
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
|
|
"[%lu] : reported pressure %.5f even though pen is not touching surface",
|
|
(unsigned long) ev->timestamp, last_pressure);
|
|
|
|
}
|
|
last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK;
|
|
last_button = ev->pen_state & 0xf; /* button mask */
|
|
if ((ev->pen_state & SDL_PEN_DOWN_MASK) && !last_touching) {
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
|
|
"[%lu] : reported flags %x (SDL_PEN_FLAG_DOWN_MASK) despite not receiving SDL_EVENT_PEN_DOWN",
|
|
(unsigned long) ev->timestamp, ev->pen_state);
|
|
|
|
}
|
|
if (!(ev->pen_state & SDL_PEN_DOWN_MASK) && last_touching) {
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_TEST,
|
|
"[%lu] : reported flags %x (no SDL_PEN_FLAG_DOWN_MASK) despite receiving SDL_EVENT_PEN_DOWN without SDL_EVENT_PEN_UP afterwards",
|
|
(unsigned long) ev->timestamp, ev->pen_state);
|
|
|
|
}
|
|
#if VERBOSE
|
|
SDL_Log("[%lu] pen button: %s %u at (%.4f, %.4f); BUTTON %d reported %s with event %s [pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f]\n",
|
|
(unsigned long) ev->timestamp,
|
|
last_was_eraser ? "eraser" : "pen",
|
|
(unsigned int)ev->which, ev->x, ev->y,
|
|
ev->button,
|
|
(ev->state == SDL_PRESSED) ? "PRESSED"
|
|
: ((ev->state == SDL_RELEASED) ? "RELEASED" : "--invalid--"),
|
|
event.type == SDL_EVENT_PEN_BUTTON_UP ? "PENBUTTONUP" : "PENBUTTONDOWN",
|
|
last_pressure, last_xtilt, last_ytilt, last_distance);
|
|
#endif
|
|
} break;
|
|
|
|
case SDL_EVENT_WINDOW_PEN_ENTER:
|
|
SDL_Log("[%lu] Pen %lu entered window %lx",
|
|
(unsigned long) event.window.timestamp,
|
|
(unsigned long) event.window.data1,
|
|
(unsigned long) event.window.windowID);
|
|
break;
|
|
|
|
case SDL_EVENT_WINDOW_PEN_LEAVE:
|
|
SDL_Log("[%lu] Pen %lu left window %lx",
|
|
(unsigned long) event.window.timestamp,
|
|
(unsigned long) event.window.data1,
|
|
(unsigned long) event.window.windowID);
|
|
break;
|
|
|
|
#if VERBOSE
|
|
case SDL_EVENT_WINDOW_MOUSE_ENTER:
|
|
SDL_Log("[%lu] Mouse entered window %lx",
|
|
(unsigned long) event.window.timestamp,
|
|
(unsigned long) event.window.windowID);
|
|
break;
|
|
|
|
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
|
SDL_Log("[%lu] Mouse left window %lx",
|
|
(unsigned long) event.window.timestamp,
|
|
(unsigned long) event.window.windowID);
|
|
break;
|
|
#endif
|
|
|
|
case SDL_EVENT_FINGER_DOWN:
|
|
case SDL_EVENT_FINGER_MOTION:
|
|
case SDL_EVENT_FINGER_UP:
|
|
{
|
|
SDL_TouchFingerEvent *ev = &event.tfinger;
|
|
int w, h;
|
|
SDL_HideCursor();
|
|
SDL_GetWindowSize(SDL_GetWindowFromID(ev->windowID), &w, &h);
|
|
last_x = ev->x * w;
|
|
last_y = ev->y * h;
|
|
update_axes_from_touch(ev->pressure);
|
|
last_was_eraser = SDL_FALSE;
|
|
last_button = 0;
|
|
last_touching = (ev->type != SDL_EVENT_FINGER_UP);
|
|
#if VERBOSE
|
|
SDL_Log("[%lu] finger %s: %s (touchId: %" SDL_PRIu64 ", fingerId: %" SDL_PRIu64 ") at (%.4f, %.4f); pressure=%.3f\n",
|
|
(unsigned long) ev->timestamp,
|
|
ev->type == SDL_EVENT_FINGER_DOWN ? "down" : (ev->type == SDL_EVENT_FINGER_MOTION ? "motion" : "up"),
|
|
SDL_GetTouchDeviceName(ev->touchId),
|
|
ev->touchId,
|
|
ev->fingerId,
|
|
last_x, last_y, last_pressure);
|
|
#endif
|
|
} break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void loop(void)
|
|
{
|
|
SDL_Event event;
|
|
int i;
|
|
|
|
for (i = 0; i < state->num_windows; ++i) {
|
|
if (state->renderers[i]) {
|
|
DrawScreen(state->renderers[i]);
|
|
}
|
|
}
|
|
|
|
if (SDL_WaitEventTimeout(&event, 10)) {
|
|
process_event(event);
|
|
}
|
|
while (SDL_PollEvent(&event)) {
|
|
process_event(event);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO);
|
|
if (!state) {
|
|
return 1;
|
|
}
|
|
|
|
state->window_title = "Pressure-Sensitive Pen Test";
|
|
state->window_w = WIDTH;
|
|
state->window_h = HEIGHT;
|
|
state->skip_renderer = SDL_FALSE;
|
|
|
|
if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) {
|
|
SDLTest_CommonQuit(state);
|
|
return 1;
|
|
}
|
|
|
|
while (!quitting) {
|
|
loop();
|
|
}
|
|
|
|
SDLTest_CommonQuit(state);
|
|
return 0;
|
|
}
|