Ayuda con ASM de Z80

Página 1/2
| 2

Por saintdboy

Supporter (6)

Imagen del saintdboy

18-11-2021, 02:54

Estimados, como están?
Después de muchos años de retrasarlo, me puse a aprender ASM para Z80. La idea es ir aprendiendo de a poco como programar un juego en una MSX 1.
Como programador, pero de lenguajes de alto nivel, entiendo que el tema de la optimización es aprendizaje y un poco de arte.
Por eso quería saber, si alguno de ustedes, con su tiempo, podrían darme algunos tips en este programa MUY chiquito que hice. El objetivo del programa es mover un sprite en ocho direcciones con las teclas del cursor.
Cualquier consejo de optimización básicas son mas que bienvenidos! Seguramente hice muchas cosas manera que se podría hacer muchísimo mas eficiente o que se debían de hacer de otra manera.

A continuación pego el código, tiene muchos comentarios, algunos obvios porque estoy aprendiendo los modos de direccionamiento, saltos y otras cosas.
No se si suma, pero uso SJASM para compilar y el programa es para cargar desde el BASIC.

Muchas gracias todos los que se tomaron su tiempo en leer este mensaje!

Saludos!

Saintdboy

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Mueve un personaje por la pantalla en base a la tecla direccional presionada  ;
;Incluye una rutina para esperar el V_BLANK y movimiento en ocho direcciones   ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;https://konamiman.github.io/MSX2-Technical-Handbook/md/Chapter5a.html#3-keyboard-interface
;LDIRVM Address  : #005C
;Function : Block transfer to VRAM from memory
;Input    : BC - Block length
;           DE - Start address of VRAM
;           HL - Start address of memory
;--------------------------------------
;CHGMOD Address  : #005F
;Function : Switches to given screen mode
;Input    : A  - Screen mode
;--------------------------------------
;WRTVDP Address  : #0047
;Function : Write data in the VDP-register
;Input    : B  - Data to write
;           C  - Number of the register
;Registers: AF, BC
;--------------------------------------
;SNSMAT Address  : #0141
;Function : Returns the value of the specified line from the keyboard matrix
;Input    : A  - For the specified line
;Output   : A  - For data (the bit corresponding to the pressed key will be 0)
;Registers: AF
;-------------------------------------
VRAM_SPRITE_PATTERN 	equ #3800 ;144336
VRAM_SPRITE_ATTRIBUTE 	equ #1B00 ;6912
CHGMOD 					equ #005F 
LDIRVM 					equ #005C
WRTVDP 					equ #0047; Escribe en los registros del VDP 
RG15AV 					equ #F3E0; alamcena el valor del registro 1 de escritura del VDP, hay unas rutinas de la bios que guardan es entas direcciones valores globals del sistema
SNSMAT					equ #0141
JIFFY 					equ #FC9E

KB_RIGHT 				equ 7
KB_DOWN 				equ 6
KB_UP 					equ	5
KB_LEFT 				equ	4
MOV_SPEED				equ 3

	; <a href="https://www.faq.msxnet.org/suffix.html" title="https://www.faq.msxnet.org/suffix.html">https://www.faq.msxnet.org/suffix.html</a>
	db #fe              
	dw INICIO            
	dw FINAL             
	dw MAIN               
	
	org	#8200                 
INICIO:


MAIN:
	call SET_SCREEN_MODE
	call SET_SPRITE_PATTERN
	call MAIN_LOOP
	
	ret

MAIN_LOOP:
	LD A, (JIFFY_TEMP)		;Cargamos el valor anterior del jiffy
	LD HL, (JIFFY)			;Cargamos el valor actual del jiffy
	CP (HL)					;Son iguales?
	JP Z, MAIN_LOOP			;Si son iguales, vuelvo a verificar
	
	LD A,(HL)				;Si no son iguales, entonces guardo el nuevo valor
	LD (JIFFY_TEMP), A
	call MANAGE_INPUT
	call UPDATE_SPRITE_ATTRIBUTE
	jp MAIN_LOOP			;Y escapamos al main loop

MANAGE_INPUT:
	LD A, 0
	LD (CHAR_SPEED_X), A
	LD (CHAR_SPEED_Y), A

	ld a, 8
	call SNSMAT

	BIT KB_UP, a			; La tecla presionada es UP?
	JP NZ, CHECK_KB_DOWN	; Sino es asi, continuamos viendo si toco DOWN
	PUSH AF					; Guardamos el valor de A, que es donde esta la lectura del teclado
	LD A, -MOV_SPEED
	LD (CHAR_SPEED_Y), A
	POP AF
	
CHECK_KB_DOWN:
	BIT KB_DOWN, a			; La tecla presionada es DOWN?
	JP NZ, CHECK_KB_LEFT	; Sino es DOWN, continuamos con otra parte
	PUSH AF					; Guardamos el valor de A, que es donde esta la lectura del teclado
	LD A, MOV_SPEED			
	LD (CHAR_SPEED_Y), A	; Actualizamos la velocidad del personaje
	POP AF
	
CHECK_KB_LEFT:
	BIT KB_LEFT, a			; La tecla presionada es DOWN?
	JP NZ, CHECK_KB_RIGHT	; Sino es DOWN, continuamos con otra parte
	PUSH AF					; Guardamos el valor de A, que es donde esta la lectura del teclado
	LD A, -MOV_SPEED
	LD (CHAR_SPEED_X), A	; Actualizamos la velocidad del personaje
	POP AF
	
CHECK_KB_RIGHT:
	BIT KB_RIGHT, a			; La tecla presionada es DOWN?
	JP NZ, UPDATE_MOVEMENT	; Sino es DOWN, continuamos con otra parte
	LD A, MOV_SPEED
	LD (CHAR_SPEED_X), A	; Actualizamos la velocidad del personaje
	
UPDATE_MOVEMENT
	LD A, (CHAR_Y)			
	LD HL, (CHAR_SPEED_Y)
	LD B, L
	ADD B					; Actualizamos la posicion en base a la velocidad
	LD (CHAR_Y), A			
	
	LD A, (CHAR_X)			
	LD HL, (CHAR_SPEED_X)
	LD B, L
	ADD B					; Actualizamos la posicion en base a la velocidad
	LD (CHAR_X), A			
	ret
	
SET_SCREEN_MODE:
	;Cambiamos el modo de pantalla
	ld  a,2     			; La rutina CHGMOD nos obliga a poner en el registro a el modo de pantalla que queremos
	call CHGMOD 			

	ld a,(RG15AV) 			; esta direcciónd e memoria alamcena el valor el registro de lectura del VDP
	or 00000011b 			; con or poniendo un 0 siempre respetamos los bits que hay y poniendo 1 1 obligamos a que sea 1

	ld b,a 					; carga en b el valor de a
	ld c,1 					; La rutina WRTVDP necesta que le carguemos previamente el entero en C del z80 del registro que queroms modificar
	call WRTVDP 			; Escribe en los registros del VDP 
	ret

SET_SPRITE_PATTERN:
	ld bc, 8*4
	ld de, VRAM_SPRITE_PATTERN
	ld hl, SPRITES_DATA
	call  LDIRVM
	ret
	
UPDATE_SPRITE_ATTRIBUTE:
	ld bc,4
	ld de, VRAM_SPRITE_ATTRIBUTE
	ld hl, CHAR_ATTRIBUTE
	call  LDIRVM

SPRITES_DATA:
	; sprite 1 (16x16)
	DB $03,$03,$03,$1F,$17,$17,$17,$17
	DB $17,$07,$04,$04,$04,$04,$04,$0C
	DB $00,$00,$00,$E0,$A0,$A0,$A0,$A0
	DB $A0,$80,$80,$80,$80,$80,$80,$C0
	

;Definición de stributos sprite, a esto se le llama plano y cada plano tiene 4 bytes, solo nos caben 32 planos en el espacio de la VRAM
CHAR_ATTRIBUTE:
CHAR_Y DB $64
CHAR_X DB $64
SPRITE_NUMBER DB $00
COLOR DB $07; aqui se defien el color y el early clock (que es para desparecer el sprite)

JIFFY_TEMP DB $00
CHAR_SPEED_X DB $00
CHAR_SPEED_Y DB $00

FINAL:
;        7       6       5       4       3       2       1       0
;    -----------------------------------------------------------------
; 0  |   7   |   6   |   5   |   4   |   3   |   2   |   1   |   0   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 1  |   ;   |   ]   |   [   |   \   |   =   |   -   |   9   |   8   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 2  |   B   |   A   | accent|   /   |   .   |   ,   |   `   |   '   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 3  |   J   |   I   |   H   |   G   |   F   |   E   |   D   |   C   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 4  |   R   |   Q   |   P   |   O   |   N   |   M   |   L   |   K   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 5  |   Z   |   Y   |   X   |   W   |   V   |   U   |   T   |   S   |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 6  |   F3  |   F2  |   F1  | CODE  | CAPS  | GRAPH | CTRL  | SHIFT |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 7  | RETURN| SELECT|   BS  | STOP  |  TAB  |  ESC  |   F5  |   F4  |
;    |-------+-------+-------+-------+-------+-------+-------+-------|
; 8  | RIGHT | DOWN  |   UP  | LEFT  |  DEL  |  INS  | HOME  | SPACE |
;    -----------------------------------------------------------------
Login sesión o register para postear comentarios

Por Juan Luis

Master (147)

Imagen del Juan Luis

18-11-2021, 13:06

Yo he compilado el programa y funciona perfectamente. Se ve un sprite de un "hombrecillo" de color celeste que se mueve perfectamente por la pantalla al pulsar los cursores. El único detalle a mejorar es que cuando el sprite se acerca al borde izquierdo de la pantalla, desaparece súbitamente antes de llegar al borde.

Lo puedes arreglar activando el bit de early-clock del VDP. Fernando García lo explica muy bien en sus vídeo. Desgraciadamente, Fernando ha ocultado los vídeos y la única manera de acceder a sus vídeos antiguos es mediante este enlace:

Curso de Ensamblador de Fernando García

No recuerdo bien si lo explica lo del "early clock" para situar sprites en el borde izquierdo en el vídeo número 12, 13 o 20. Echa un vistazo, y seguro que mejoras ese detalle.

Personalmente, encuentro el código muy bien documentado, muy claro y legible, y además funciona muy bien.

Mis felicitaciones.

Por theNestruo

Champion (430)

Imagen del theNestruo

19-11-2021, 19:18

Buenas!

Te comento detallitos sobre tu código.
Algunas cosas son irrelevantes en tu código ahora mismo, pero puede venir bien el futuro (y yo soy de los que piensan que cuanto antes cojas manías "buenas", mejor!).

saintdboy wrote:
MAIN:
	call SET_SCREEN_MODE
	call SET_SPRITE_PATTERN
	call MAIN_LOOP

Como decía, aquí es irrelevante, pero te aconsejo añadir un call DISSCR antes de toda la inicialización gráfica y un call ENASCR cuando la hayas terminado. Cuando empieces a redefinir caracteres, limpiar la pantalla, etc. te asegurarás que no se vea "basurilla" en pantalla aunque sólo sea un frame.

saintdboy wrote:
	call MAIN_LOOP
	ret

Salvo que tengas rutinas que "jueguen" con la pila (y no es tu caso), un call ABC : ret se puede sustituir por un jp ABC. Te ahorras un byte, un puñado de ciclos, y reduces el consumo de pila. De nuevo, aquí es irrelevante, pero a la larga se nota.

saintdboy wrote:
MAIN_LOOP:
	LD A, (JIFFY_TEMP)		;Cargamos el valor anterior del jiffy
	LD HL, (JIFFY)			;Cargamos el valor actual del jiffy
	CP (HL)					;Son iguales?
	JP Z, MAIN_LOOP			;Si son iguales, vuelvo a verificar
	
	LD A,(HL)				;Si no son iguales, entonces guardo el nuevo valor
	LD (JIFFY_TEMP), A

Como no has deshabilitado interrupciones, todo este bloque de código se puede sustituir por halt! Aunque no es del todo cierto, puedes considerar que la única interrupción que hay en un MSX es la del refresco de pantalla (que es justo en la que la BIOS actualiza JIFFY).

saintdboy wrote:
	LD A, 0
	LD (CHAR_SPEED_X), A
	LD (CHAR_SPEED_Y), A

Una optimizatión muy común en Z80 es reemplazar LD A, 0 por XOR A que también te pone A a 0 (con el efecto colateral de modificar los flags, pero en tu caso no lo necesitas).

saintdboy wrote:
	LD A, (CHAR_Y)			
	LD HL, (CHAR_SPEED_Y)
	LD B, L
	ADD B					; Actualizamos la posicion en base a la velocidad
	LD (CHAR_Y), A			

Aquí no tengo muy claro por qué pasar por B para sumarlo a A... Existe la operacion ADD L que te ahorraría esa asignación y ese registro intermedio.

saintdboy wrote:
	LD A, (CHAR_X)			
	LD HL, (CHAR_SPEED_X)
	LD B, L
	ADD B					; Actualizamos la posicion en base a la velocidad
	LD (CHAR_X), A			
	ret

Lo mismo que en el caso anterior: no necesitas B.

Realmente estos dos bloques se pueden optimizar bastante aprovechando que las variables están seguidas en la memoria:

	LD BC, (CHAR_SPEED_X)	; El valor de CHAR_SPEED_X se carga en C y el de CHAR_SPEED_Y se carga en B
	LD HL, CHAR_X
	LD A, (HL)
	ADD C					; Actualizamos la posicion en base a la velocidad
	LD (HL), A
	INC HL					; Ahora HL apunta a CHAR_Y
	LD A, (HL)
	ADD B					; Actualizamos la posicion en base a la velocidad
	LD (HL), A

Pero vamos, si estás aprendiendo es mejor que hagas código limpio y claro antes de intentar buscar el máximo rendimiento en cada rutina. Sobre todo porque el código acaba siendo menos legible y realmente lo que acabas haciendo es entorpercer tu aprendizaje.

saintdboy wrote:
SET_SCREEN_MODE:
	;Cambiamos el modo de pantalla
	ld  a,2     			; La rutina CHGMOD nos obliga a poner en el registro a el modo de pantalla que queremos
	call CHGMOD 			

En la BIOS también existe la rutina call INIGRP que ahorra los dos bytes del ld a,2... pero no sé por qué se utiliza tanto esta construcción usando la genérica CHGMOD.

saintdboy wrote:
	ld a,(RG15AV) 			; esta direcciónd e memoria alamcena el valor el registro de lectura del VDP
	or 00000011b 			; con or poniendo un 0 siempre respetamos los bits que hay y poniendo 1 1 obligamos a que sea 1
	
	ld b,a 					; carga en b el valor de a
	ld c,1 					; La rutina WRTVDP necesta que le carguemos previamente el entero en C del z80 del registro que queroms modificar
	call WRTVDP 			; Escribe en los registros del VDP 
	ret

Esto en tu código te vale para hacer efectivo el cambio de valor en el registro VDP#1. Pero si sigues mi consejo de más arriba (el de inicializar con la pantalla deshabilitada y luego hacer call ENASCR), ENASCR va a coger el valor de RG1SAV, por lo que puedes ahorrarte unos cuantos bytes haciendo:

	ld	hl, RG1SAV
	ld	a, [hl]
	or	00000011b
	ld	[hl], a

De hecho, esto es lo que hago yo para inicializar la pantalla en modo SCREEN2,2:

; screen 2
	call	INIGRP
; screen ,2
	call	DISSCR
	ld	hl, RG1SAV
	set	1, [hl] ; (first call to ENASCR will actually apply to the VDP)
saintdboy wrote:
UPDATE_SPRITE_ATTRIBUTE:
	ld bc,4
	ld de, VRAM_SPRITE_ATTRIBUTE
	ld hl, CHAR_ATTRIBUTE
	call  LDIRVM

SPRITES_DATA:
	; sprite 1 (16x16)
	DB $03,$03,$03,$1F,$17,$17,$17,$17
	DB $17,$07,$04,$04,$04,$04,$04,$0C
	DB $00,$00,$00,$E0,$A0,$A0,$A0,$A0
	DB $A0,$80,$80,$80,$80,$80,$80,$C0

Ojo aquí, que te falta un RET después del call LDIRVM, o sustituir el call por un jp LDIRVM. De lo contrario te va a ejecutar los bytes de datos que vienen después como si fueran código!

Por Juan Luis

Master (147)

Imagen del Juan Luis

19-11-2021, 21:27

Es cierto, no lo había visto lo de que falta un ret en la rutina UPDATE_SPRITE_ATTRIBUTE;, pero yo he ensamblado el programa y lo he probado en el OpenMSX y a mí me funciona, y he podido ver el sprite e incluso controlarlo con el teclado.

¿Cómo es posible?

Por theNestruo

Champion (430)

Imagen del theNestruo

19-11-2021, 23:13

Juan Luis wrote:

Es cierto, no lo había visto lo de que falta un ret en la rutina UPDATE_SPRITE_ATTRIBUTE;, pero yo he ensamblado el programa y lo he probado en el OpenMSX y a mí me funciona, y he podido ver el sprite e incluso controlarlo con el teclado.

¿Cómo es posible?

Chiripa. Han coincidido los bytes con instrucciones inocuas: $03 es INC BC, $1F es RRA, $17 es RLA, etc... Y en algún momento habrá ejecutado un RET (puede que en el $C0, que es RET NZ). Pero podía haber sido cualquier cosa Big smile

Por saintdboy

Supporter (6)

Imagen del saintdboy

21-11-2021, 04:08

Muchas gracias theNuestro!!!
Seguí todos los consejos que me tiraste. Se agradece mucho!
Lo único es no pude hacer que el ENASCR vuelque en el registro del VDP el contenido del HL.
Sera la versión de BIOS que estoy usando?
Estoy usando BLUEMSX con el BIOS que trae para MSX1!

Fue muy gracioso lo del RET que faltaba!!!
Ahora entiendo porque cuando cambiaba el sprite con determinados bytes no funcionaba!!! Big smile Big smile

Por saintdboy

Supporter (6)

Imagen del saintdboy

21-11-2021, 04:09

Juan Luis wrote:

Es cierto, no lo había visto lo de que falta un ret en la rutina UPDATE_SPRITE_ATTRIBUTE;, pero yo he ensamblado el programa y lo he probado en el OpenMSX y a mí me funciona, y he podido ver el sprite e incluso controlarlo con el teclado.

¿Cómo es posible?

Gracias Juan Luis por la onda!!!
Es verdad! Fue de pura suerte! De hecho, no cambiaba el sprite, porque cuando lo cambiaba habia determinados bytes que me terminaban reiniciando la MSX jajajaja Me estaba volviendo loco! No entendia nada!
Fue pura suerte!!!

Por theNestruo

Champion (430)

Imagen del theNestruo

21-11-2021, 12:27

saintdboy wrote:

Lo único es no pude hacer que el ENASCR vuelque en el registro del VDP el contenido del HL.

No, no; me expliqué mal: lo que vuelca ENASCR es lo que esté guardado en RG1SAV (que en el fragmentito que puse se guarda con el ld [hl], a).
Es decir: la BIOS se fia de que lo que ha dejado en las variables RGnSAV es el valor que supuestamente tiene el VDP... así que las operaciones como DISSCR, ENASCR, etc. cogen el valor de ahí, tocan el bit que necesiten tocar, y lo mandan al VDP. Como en el caso de ENASCR utiliza el mismo registro en el que está el bit que indica el tipo de sprites, pues se puede aprovechar ^_^

Por saintdboy

Supporter (6)

Imagen del saintdboy

23-11-2021, 19:44

Ahhh Muchas gracias por la aclaración!!! Lo voy a implementar!!!

Por otro lado, dos preguntas:
1) Tengo entendido que la VDP de MSX no soporta mirror de SPRITES, es así? Como se hace cuando uno quiere hacer un flip de sprites entonces? Tienen guardado el sprite espejado?? Estoy usando un sprite doble de 16x16 para dos colores. Entonces, para cada frame de animación, son 64 bytes. Si cuento 3 frames para la derecha para el loop de animación, 3 a la izquierda, 1 de estarse quieto y uno de salto, tendría la nada despreciable suma de medio KB solo de animación de personaje! Vi técnicas de mirroring o son lentas o usan una tabla lookup para invertir los bytes. Como hacian antes?
2) Tenes un ejemplo practico de como hacer un if-else? Me encuentro poniendo muchas etiquetas por todos lados para hacer los JP, no se si esto es lo normal o no, se me están acabando los nombres ya jajajaja.

Mil gracias a todos!!!

Por theNestruo

Champion (430)

Imagen del theNestruo

23-11-2021, 23:09

saintdboy wrote:

1) Tengo entendido que la VDP de MSX no soporta mirror de SPRITES, es así? Como se hace cuando uno quiere hacer un flip de sprites entonces? Tienen guardado el sprite espejado?? Estoy usando un sprite doble de 16x16 para dos colores. Entonces, para cada frame de animación, son 64 bytes. Si cuento 3 frames para la derecha para el loop de animación, 3 a la izquierda, 1 de estarse quieto y uno de salto, tendría la nada despreciable suma de medio KB solo de animación de personaje! Vi técnicas de mirroring o son lentas o usan una tabla lookup para invertir los bytes. Como hacian antes

Efectivamente, no hay mirroring de sprites. Aquí tienes dos opciones: o tener cargados en la definición de los patrones todas las variantes (eso es: consumir "muchos" patrones) o redefinir los patrones en cada frame. Si optas por lo primero, yo personalmente dejaría que el compresor hiciera su trabajo y me despreocuparía; pero si prefieres implementar mirroring puedes hacerte una rutina pequeña y lenta (porque la carga de los patrones se hará una vez). Si optas por lo segundo, necesitarás una rutina que sea capaz de volvar a VRAM un sprite de manera normal o reflejada, y que sea rápida (lo que normalmente se traduce en que ocupará bastante).
Si quieres ver cómo se hacía antes... te aconsejo que pruebes los emuladores meisei (sólo Windows) y Emulicious (multiplataforma), ya que te permiten cotillear la VRAM de manera muy fácil. Verás que hay juegos que lo hacen de una manera, de otra, una combinación de las dos...

saintdboy wrote:

2) Tenes un ejemplo practico de como hacer un if-else? Me encuentro poniendo muchas etiquetas por todos lados para hacer los JP, no se si esto es lo normal o no, se me están acabando los nombres ya jajajaja.

Me suena que hay algún ensamblador que permite etiquetas "anónimas" (tipo .1, .2, .3 ...), pero no suele ser el caso y sí, al final acabas con un montón de etiquetas muy poco descriptivas. No hay remedio para eso. Yo a veces utilizo "jr z, $+3" y construcciones similares para ahorrarme una etiqueta, pero es casi peor el remedio que la enfermedad.

Respecto a ejemplos, recientemente he liberado Stevedore, así como su código fuente; siéntete libre de echarle un vistazo. El ensamblador es tniASM v0.45, así que debería ser código Z80 bastante fácil de seguir (no hay pseudoinstrucciones ni macros).
Para que no parezca que "barro para casa", en GitHub también puedes encontrar proyectos más molones que Stevedore, como el impresionante The Sword of Ianna, las maravillas que hace Santi Ontañón o el mismísimo MetalGear. Pero opino que como proyectos "de ejemplo" o "para aprender" igual resultan demasiado grandes y/o complejos.

Por cierto, si en Stevedore tienes un ejemplo de tener todos los patrones de sprites cargados (en ambas direcciones), en el inacabado World Rally (en este caso en sintaxis asMSX) tienes un ejemplo de la técnica contraria: los sprites del coche se redefinían en cada frame.

Por saintdboy

Supporter (6)

Imagen del saintdboy

24-11-2021, 00:56

Muchas gracias por todos tus comentarios. Llevo tantos años programando en C# que bajar a assembler cuesta un poco, tengo que cambiar el "cassette" mental. (por el tema de los if-else digo)
Con respecto a los fuentes que me pasas, se agradece un monton! si el codigo esta documentado algo se puede obtener Wink

Tu juego es HERMOSO. Si lo hubiese tenido de chico me habria volado la cabeza!!!! Que lastima que esté en europa la venta de cartuchos, me encantaria tener uno. Pero con los impuestos de mi pais (Argentina) podria llegar a quedar unos 80 euros entre impuestos y aduana, mas un impuesto a la moneda extranjera es incomprable ya Sad Pero bueno, es el pais que me toco.

Muchas gracias de nuevo!

Página 1/2
| 2