|
|
|
#include <assert.h>
|
|
|
|
#include <rfb/rfb.h>
|
|
|
|
#include <rfb/rfbclient.h>
|
|
|
|
|
|
|
|
#include "nacro.h"
|
|
|
|
|
|
|
|
/* for visual grepping */
|
|
|
|
typedef struct image_t {
|
|
|
|
int width,height;
|
|
|
|
char* buffer;
|
|
|
|
} image_t;
|
|
|
|
|
|
|
|
/* this is a VNC connection */
|
|
|
|
typedef struct private_resource_t {
|
|
|
|
int listen_port;
|
|
|
|
rfbScreenInfo* server;
|
|
|
|
rfbClient* client;
|
|
|
|
|
|
|
|
uint32_t keysym;
|
|
|
|
rfbBool keydown;
|
|
|
|
|
|
|
|
int x,y;
|
|
|
|
int buttons;
|
|
|
|
|
|
|
|
image_t* grep_image;
|
|
|
|
int x_origin,y_origin;
|
|
|
|
|
|
|
|
enum { SLEEP,VISUALGREP,WAITFORUPDATE } state;
|
|
|
|
result_t result;
|
|
|
|
} private_resource_t;
|
|
|
|
|
|
|
|
/* resource management */
|
|
|
|
|
|
|
|
#define MAX_RESOURCE_COUNT 20
|
|
|
|
|
|
|
|
static private_resource_t resource_pool[MAX_RESOURCE_COUNT];
|
|
|
|
static int resource_count=0;
|
|
|
|
|
|
|
|
private_resource_t* get_resource(int resource)
|
|
|
|
{
|
|
|
|
if(resource>=MAX_RESOURCE_COUNT || resource<0 || resource_pool[resource].client==0)
|
|
|
|
return 0;
|
|
|
|
return resource_pool+resource;
|
|
|
|
}
|
|
|
|
|
|
|
|
private_resource_t* get_next_resource()
|
|
|
|
{
|
|
|
|
if(resource_count<MAX_RESOURCE_COUNT) {
|
|
|
|
memset(resource_pool+resource_count,0,sizeof(private_resource_t));
|
|
|
|
resource_count++;
|
|
|
|
return resource_pool+resource_count-1;
|
|
|
|
} else {
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for(i=0;i<MAX_RESOURCE_COUNT && resource_pool[i].client;i++);
|
|
|
|
if(i<MAX_RESOURCE_COUNT)
|
|
|
|
return resource_pool+i;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void free_resource(int resource)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
if(res)
|
|
|
|
res->client=0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* hooks */
|
|
|
|
|
|
|
|
void got_key(rfbBool down,rfbKeySym keysym,rfbClientRec* cl)
|
|
|
|
{
|
|
|
|
private_resource_t* res=(private_resource_t*)cl->screen->screenData;
|
|
|
|
|
|
|
|
res->keydown=down;
|
|
|
|
res->keysym=keysym;
|
|
|
|
res->result|=RESULT_KEY;
|
|
|
|
}
|
|
|
|
|
|
|
|
void got_mouse(int buttons,int x,int y,rfbClientRec* cl)
|
|
|
|
{
|
|
|
|
private_resource_t* res=(private_resource_t*)cl->screen->screenData;
|
|
|
|
|
|
|
|
res->buttons=buttons;
|
|
|
|
res->x=x;
|
|
|
|
res->y=y;
|
|
|
|
res->result|=RESULT_MOUSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
rfbBool malloc_frame_buffer(rfbClient* cl)
|
|
|
|
{
|
|
|
|
private_resource_t* res=(private_resource_t*)cl->clientData;
|
|
|
|
|
|
|
|
if(!res->server) {
|
|
|
|
int w=cl->width,h=cl->height;
|
|
|
|
|
|
|
|
res->client->frameBuffer=malloc(w*4*h);
|
|
|
|
|
|
|
|
res->server=rfbGetScreen(0,0,w,h,8,3,4);
|
|
|
|
res->server->screenData=res;
|
|
|
|
res->server->port=res->listen_port;
|
|
|
|
res->server->frameBuffer=res->client->frameBuffer;
|
|
|
|
res->server->kbdAddEvent=got_key;
|
|
|
|
res->server->ptrAddEvent=got_mouse;
|
|
|
|
rfbInitServer(res->server);
|
|
|
|
} else {
|
|
|
|
/* TODO: realloc if necessary */
|
|
|
|
/* TODO: resolution change: send NewFBSize */
|
|
|
|
/* TODO: if the origin is out of bounds, reset to 0 */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool_t do_visual_grep(private_resource_t* res,int x,int y,int w,int h)
|
|
|
|
{
|
|
|
|
rfbClient* cl;
|
|
|
|
image_t* image;
|
|
|
|
int x_start,y_start,x_end=x+w-1,y_end=y+h-1;
|
|
|
|
bool_t found=0;
|
|
|
|
|
|
|
|
if(res==0 || (cl=res->client)==0 || (image=res->grep_image)==0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
x_start=x-image->width;
|
|
|
|
y_start=y-image->height;
|
|
|
|
if(x_start<0) x_start=0;
|
|
|
|
if(y_start<0) y_start=0;
|
|
|
|
if(x_end+image->width>cl->width) x_end=cl->width-image->width;
|
|
|
|
if(y_end+image->height>cl->height) y_end=cl->height-image->height;
|
|
|
|
|
|
|
|
/* find image and set x_origin,y_origin if found */
|
|
|
|
for(y=y_start;y<y_end;y++)
|
|
|
|
for(x=x_start;x<x_end;x++) {
|
|
|
|
bool_t matching=1;
|
|
|
|
int i,j;
|
|
|
|
for(j=0;matching && j<image->height;j++)
|
|
|
|
for(i=0;matching && i<image->width;i++)
|
|
|
|
if(memcmp(cl->frameBuffer+4*(x+i+cl->width*(y+j)),image->buffer+4*(i+image->width*j),3))
|
|
|
|
matching=0;
|
|
|
|
if(matching) {
|
|
|
|
private_resource_t* res=(private_resource_t*)cl->clientData;
|
|
|
|
res->x_origin=x;
|
|
|
|
res->y_origin=y;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void got_frame_buffer(rfbClient* cl,int x,int y,int w,int h)
|
|
|
|
{
|
|
|
|
private_resource_t* res=(private_resource_t*)cl->clientData;
|
|
|
|
|
|
|
|
assert(res->server);
|
|
|
|
|
|
|
|
if(res->grep_image && do_visual_grep(res,x,y,w,h)) {
|
|
|
|
res->result|=RESULT_FOUNDIMAGE;
|
|
|
|
}
|
|
|
|
if(res->server) {
|
|
|
|
rfbMarkRectAsModified(res->server,x,y,x+w,y+h);
|
|
|
|
}
|
|
|
|
|
|
|
|
res->result|=RESULT_SCREEN;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* init/shutdown functions */
|
|
|
|
|
|
|
|
resource_t initvnc(const char* server,int server_port,int listen_port)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_next_resource();
|
|
|
|
int dummy=0;
|
|
|
|
|
|
|
|
if(res==0)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
/* remember for later */
|
|
|
|
res->listen_port=listen_port;
|
|
|
|
|
|
|
|
res->client=rfbGetClient(8,3,4);
|
|
|
|
res->client->clientData=res;
|
|
|
|
res->client->GotFrameBufferUpdate=got_frame_buffer;
|
|
|
|
res->client->MallocFrameBuffer=malloc_frame_buffer;
|
|
|
|
res->client->serverHost=strdup(server);
|
|
|
|
res->client->serverPort=server_port;
|
|
|
|
res->client->appData.encodingsString="raw";
|
|
|
|
if(!rfbInitClient(res->client,&dummy,0)) {
|
|
|
|
res->client=0;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return res-resource_pool;
|
|
|
|
}
|
|
|
|
|
|
|
|
void closevnc(resource_t resource)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
if(res==0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(res->server)
|
|
|
|
rfbScreenCleanup(res->server);
|
|
|
|
|
|
|
|
assert(res->client);
|
|
|
|
|
|
|
|
rfbClientCleanup(res->client);
|
|
|
|
|
|
|
|
res->client=0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* PNM (image) helpers */
|
|
|
|
|
|
|
|
bool_t savepnm(resource_t resource,const char* filename,int x1,int y1,int x2,int y2)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
int i,j,w,h;
|
|
|
|
uint32_t* buffer;
|
|
|
|
FILE* f;
|
|
|
|
|
|
|
|
if(res==0 || res->client==0)
|
|
|
|
return 0;
|
|
|
|
assert(res->client->format.depth==24);
|
|
|
|
|
|
|
|
w=res->client->width;
|
|
|
|
h=res->client->height;
|
|
|
|
buffer=(uint32_t*)res->client->frameBuffer;
|
|
|
|
|
|
|
|
if(res==0 || x1>x2 || y1>y2 || x1<0 || x2>=w || y1<0 || y2>=h)
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
f=fopen(filename,"wb");
|
|
|
|
|
|
|
|
if(f==0)
|
|
|
|
return FALSE;
|
|
|
|
|
|
|
|
fprintf(f,"P6\n%d %d\n255\n",1+x2-x1,1+y2-y1);
|
|
|
|
for(j=y1;j<=y2;j++)
|
|
|
|
for(i=x1;i<=x2;i++) {
|
|
|
|
fwrite(buffer+i+j*w,3,1,f);
|
|
|
|
}
|
|
|
|
if(fclose(f))
|
|
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
image_t* loadpnm(const char* filename)
|
|
|
|
{
|
|
|
|
FILE* f=fopen(filename,"rb");
|
|
|
|
char buffer[1024];
|
|
|
|
int i,j,w,h;
|
|
|
|
image_t* image;
|
|
|
|
|
|
|
|
if(f==0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
if(!fgets(buffer,1024,f) || strcmp("P6\n",buffer)) {
|
|
|
|
fclose(f);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
fgets(buffer,1024,f);
|
|
|
|
if(feof(f)) {
|
|
|
|
fclose(f);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
} while(buffer[0]=='#');
|
|
|
|
|
|
|
|
if( sscanf(buffer,"%d %d",&w,&h)!=2
|
|
|
|
|| !fgets(buffer,1024,f) || strcmp("255\n",buffer)) {
|
|
|
|
fclose(f);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
image=(image_t*)malloc(sizeof(image_t));
|
|
|
|
image->width=w;
|
|
|
|
image->height=h;
|
|
|
|
image->buffer=malloc(w*4*h);
|
|
|
|
if(!image->buffer) {
|
|
|
|
fclose(f);
|
|
|
|
free(image);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
for(j=0;j<h;j++)
|
|
|
|
for(i=0;i<w;i++)
|
|
|
|
if(fread(image->buffer+4*(i+w*j),3,1,f)!=1) {
|
|
|
|
fprintf(stderr,"Could not read 3 bytes at %d,%d\n",i,j);
|
|
|
|
fclose(f);
|
|
|
|
free(image->buffer);
|
|
|
|
free(image);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose(f);
|
|
|
|
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
|
|
|
|
void free_image(image_t* image)
|
|
|
|
{
|
|
|
|
if(image->buffer)
|
|
|
|
free(image->buffer);
|
|
|
|
free(image);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* process() and friends */
|
|
|
|
|
|
|
|
/* this function returns only if res->result in return_mask */
|
|
|
|
result_t private_process(resource_t resource,timeout_t timeout_in_seconds,result_t return_mask)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
fd_set fds;
|
|
|
|
struct timeval tv,tv_start,tv_end;
|
|
|
|
unsigned long timeout=(unsigned long)(timeout_in_seconds*1000000UL);
|
|
|
|
int count,max_fd;
|
|
|
|
|
|
|
|
if(res==0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
assert(res->client);
|
|
|
|
|
|
|
|
gettimeofday(&tv_start,0);
|
|
|
|
res->result=0;
|
|
|
|
|
|
|
|
do {
|
|
|
|
unsigned long timeout_done;
|
|
|
|
|
|
|
|
if(res->server) {
|
|
|
|
rfbBool loop;
|
|
|
|
do {
|
|
|
|
loop=rfbProcessEvents(res->server,res->server->deferUpdateTime);
|
|
|
|
} while(loop && (res->result&return_mask)==0);
|
|
|
|
|
|
|
|
if((res->result&return_mask)!=0)
|
|
|
|
return res->result;
|
|
|
|
|
|
|
|
memcpy((char*)&fds,(const char*)&(res->server->allFds),sizeof(fd_set));
|
|
|
|
max_fd=res->server->maxFd;
|
|
|
|
} else {
|
|
|
|
FD_ZERO(&fds);
|
|
|
|
max_fd=0;
|
|
|
|
}
|
|
|
|
FD_SET(res->client->sock,&fds);
|
|
|
|
if(res->client->sock>max_fd)
|
|
|
|
max_fd=res->client->sock;
|
|
|
|
|
|
|
|
gettimeofday(&tv_end,0);
|
|
|
|
timeout_done=tv_end.tv_usec-tv_start.tv_usec+
|
|
|
|
1000000L*(tv_end.tv_sec-tv_start.tv_sec);
|
|
|
|
if(timeout_done>=timeout)
|
|
|
|
return RESULT_TIMEOUT;
|
|
|
|
|
|
|
|
tv.tv_usec=((timeout-timeout_done)%1000000);
|
|
|
|
tv.tv_sec=(timeout-timeout_done)/1000000;
|
|
|
|
|
|
|
|
count=select(max_fd+1,&fds,0,0,&tv);
|
|
|
|
if(count<0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
if(count>0) {
|
|
|
|
if(FD_ISSET(res->client->sock,&fds)) {
|
|
|
|
if(!HandleRFBServerMessage(res->client)) {
|
|
|
|
closevnc(resource);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if((res->result&return_mask)!=0)
|
|
|
|
return res->result;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
res->result|=RESULT_TIMEOUT;
|
|
|
|
return res->result;
|
|
|
|
}
|
|
|
|
} while(1);
|
|
|
|
|
|
|
|
return RESULT_TIMEOUT;
|
|
|
|
}
|
|
|
|
|
|
|
|
result_t process(resource_t res,timeout_t timeout)
|
|
|
|
{
|
|
|
|
return private_process(res,timeout,RESULT_TIMEOUT);
|
|
|
|
}
|
|
|
|
|
|
|
|
result_t waitforanything(resource_t res,timeout_t timeout)
|
|
|
|
{
|
|
|
|
return private_process(res,timeout,-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
result_t waitforinput(resource_t res,timeout_t timeout)
|
|
|
|
{
|
|
|
|
return private_process(res,timeout,RESULT_KEY|RESULT_MOUSE|RESULT_TIMEOUT);
|
|
|
|
}
|
|
|
|
|
|
|
|
result_t waitforupdate(resource_t res,timeout_t timeout)
|
|
|
|
{
|
|
|
|
return private_process(res,timeout,RESULT_SCREEN|RESULT_TIMEOUT);
|
|
|
|
}
|
|
|
|
|
|
|
|
result_t visualgrep(resource_t resource,const char* filename,timeout_t timeout)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
image_t* image;
|
|
|
|
result_t result;
|
|
|
|
|
|
|
|
if(res==0 || res->client==0)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
/* load filename and set res->grep_image to this image */
|
|
|
|
image=loadpnm(filename);
|
|
|
|
if(image==0)
|
|
|
|
return 0;
|
|
|
|
if(res->grep_image)
|
|
|
|
free_image(res->grep_image);
|
|
|
|
res->grep_image=image;
|
|
|
|
|
|
|
|
if(do_visual_grep(res,0,0,res->client->width,res->client->height))
|
|
|
|
return RESULT_FOUNDIMAGE;
|
|
|
|
|
|
|
|
result=private_process(resource,timeout,RESULT_FOUNDIMAGE|RESULT_TIMEOUT);
|
|
|
|
|
|
|
|
/* free image */
|
|
|
|
if(res->grep_image) {
|
|
|
|
free_image(res->grep_image);
|
|
|
|
res->grep_image=0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* auxiliary function for alert */
|
|
|
|
|
|
|
|
#include "default8x16.h"
|
|
|
|
|
|
|
|
void center_text(rfbScreenInfo* screen,const char* message,int* x,int* y,int* w,int* h)
|
|
|
|
{
|
|
|
|
rfbFontData* font=&default8x16Font;
|
|
|
|
const char* pointer;
|
|
|
|
int j,x1,y1,x2,y2,line_count=0;
|
|
|
|
if(message==0 || screen==0)
|
|
|
|
return;
|
|
|
|
rfbWholeFontBBox(font,&x1,&y1,&x2,&y2);
|
|
|
|
for(line_count=1,pointer=message;*pointer;pointer++)
|
|
|
|
if(*pointer=='\n')
|
|
|
|
line_count++;
|
|
|
|
|
|
|
|
*h=(y2-y1)*line_count;
|
|
|
|
assert(*h>0);
|
|
|
|
|
|
|
|
if(*h>screen->height)
|
|
|
|
*h=screen->height;
|
|
|
|
|
|
|
|
*x=0; *w=screen->width; *y=(screen->height-*h)/2;
|
|
|
|
|
|
|
|
rfbFillRect(screen,*x,*y,*x+*w,*y+*h,0xff0000);
|
|
|
|
|
|
|
|
for(pointer=message,j=0;j<line_count;j++) {
|
|
|
|
const char* eol;
|
|
|
|
int x_cur,y_cur=*y-y1+j*(y2-y1),width;
|
|
|
|
|
|
|
|
for(width=0,eol=pointer;*eol && *eol!='\n';eol++)
|
|
|
|
width+=rfbWidthOfChar(font,*eol);
|
|
|
|
if(width>screen->width)
|
|
|
|
width=screen->width;
|
|
|
|
|
|
|
|
x_cur=(screen->width-width)/2;
|
|
|
|
for(;pointer!=eol;pointer++)
|
|
|
|
x_cur+=rfbDrawCharWithClip(screen,font,
|
|
|
|
x_cur,y_cur,*pointer,
|
|
|
|
0,0,screen->width,screen->height,
|
|
|
|
0xffffffff,0xffffffff);
|
|
|
|
pointer++;
|
|
|
|
}
|
|
|
|
rfbMarkRectAsModified(screen,*x,*y,*x+*w,*y+*h);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* this is an overlay which is shown for a certain time */
|
|
|
|
|
|
|
|
result_t alert(resource_t resource,const char* message,timeout_t timeout)
|
|
|
|
{
|
|
|
|
private_resource_t* res=get_resource(resource);
|
|
|
|
char* fake_frame_buffer;
|
|
|
|
char* backup;
|
|
|
|
int x,y,w,h;
|
|
|
|
result_t result;
|
|
|
|
|
|
|
|
if(res->server==0)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
w=res->server->width;
|
|
|
|
h=res->server->height;
|
|
|
|
|
|
|
|
fake_frame_buffer=malloc(w*4*h);
|
|
|
|
if(!fake_frame_buffer)
|
|
|
|
return -1;
|
|
|
|
memcpy(fake_frame_buffer,res->server->frameBuffer,w*4*h);
|
|
|
|
|
|
|
|
backup=res->server->frameBuffer;
|
|
|
|
res->server->frameBuffer=fake_frame_buffer;
|
|
|
|
center_text(res->server,message,&x,&y,&w,&h);
|
|
|
|
fprintf(stderr,"%s\n",message);
|
|
|
|
|
|
|
|
result=waitforinput(resource,timeout);
|
|
|
|
|
|
|
|
res->server->frameBuffer=backup;
|
|
|
|
free(fake_frame_buffer);
|
|
|
|
rfbMarkRectAsModified(res->server,x,y,x+w,y+h);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
/* inspect last events */
|
|
|
|
|
|
|
|
keysym_t getkeysym(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->keysym;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool_t getkeydown(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->keydown;
|
|
|
|
}
|
|
|
|
|
|
|
|
coordinate_t getx(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->x;
|
|
|
|
}
|
|
|
|
|
|
|
|
coordinate_t gety(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->y;
|
|
|
|
}
|
|
|
|
|
|
|
|
buttons_t getbuttons(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->buttons;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* send events to the server */
|
|
|
|
|
|
|
|
bool_t sendkey(resource_t res,keysym_t keysym,bool_t keydown)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
if(r==0)
|
|
|
|
return 0;
|
|
|
|
return SendKeyEvent(r->client,keysym,keydown);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool_t sendmouse(resource_t res,coordinate_t x,coordinate_t y,buttons_t buttons)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
if(r==0)
|
|
|
|
return 0;
|
|
|
|
return SendPointerEvent(r->client,x,y,buttons);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* for visual grepping */
|
|
|
|
|
|
|
|
coordinate_t getxorigin(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->x_origin;
|
|
|
|
}
|
|
|
|
|
|
|
|
coordinate_t getyorigin(resource_t res)
|
|
|
|
{
|
|
|
|
private_resource_t* r=get_resource(res);
|
|
|
|
return r->y_origin;
|
|
|
|
}
|
|
|
|
|